Merge branch 'www-refresh' into feature/www-composer

This commit is contained in:
Saxon Fletcher committed 2026-06-04 12:26:26 +10:00
commit 734184e045
169 files changed
+13256 -1054

No files matched your search

@@ -0,0 +1,200 @@
import Examples from 'data/Examples'
import Link from 'next/link'
import { cn } from 'ui'
const EXAMPLE_LOGO: Record<string, { src: string; invert?: boolean }> = {
'Stripe Subscriptions Starter': { src: '/images/logos/frameworks/stripe.svg' },
'Next.js Starter': { src: '/images/logos/frameworks/nextjs.svg', invert: true },
'AI Chatbot': { src: '/images/logos/frameworks/openai.svg', invert: true },
'LangChain + Next.js Starter': { src: '/images/logos/frameworks/langchain.svg' },
'Flutter User Management': { src: '/images/logos/frameworks/flutter.svg' },
'Expo React Native Starter': { src: '/images/logos/frameworks/expo.svg', invert: true },
}
function SkeletonBar({ className }: { className?: string }) {
return <div className={cn(`rounded bg-foreground-muted/10`, className)} />
}
function StripeSkeleton() {
return (
<div
aria-label="Pricing page with subscription tiers"
className="flex flex-col items-center justify-center gap-3 p-5 w-full"
>
<div className="flex -mb-8">
<div className="w-24 aspect-[9/16] border flex flex-col items-center">
<div className="flex flex-col items-center justify-center gap-2 p-2">
<SkeletonBar className="w-8 h-2.5" />
<SkeletonBar className="w-16 h-2.5" />
</div>
<SkeletonBar className="w-20 aspect-square mt-4" />
</div>
<div className="w-24 aspect-[9/16] border flex flex-col items-center border-l-0 bg-[#635BFF]/5">
<div className="flex flex-col items-center justify-center gap-2 p-2">
<SkeletonBar className="w-8 h-2.5 bg-purple-800" />
<SkeletonBar className="w-16 h-2.5 bg-purple-900" />
</div>
<SkeletonBar className="w-20 aspect-square mt-4 dark:bg-muted" />
</div>
<div className="w-24 aspect-[9/16] border flex flex-col items-center border-l-0">
<div className="flex flex-col items-center justify-center gap-2 p-2">
<SkeletonBar className="w-8 h-2.5" />
<SkeletonBar className="w-16 h-2.5" />
</div>
<SkeletonBar className="w-20 aspect-square mt-4" />
</div>
</div>
</div>
)
}
function NextjsSkeleton() {
return (
<div
aria-label="Dashboard with sidebar navigation and data grid"
className="flex w-full h-full"
>
{/* Sidebar */}
<div className="w-24 border-r border-foreground-muted/10 flex flex-col gap-2 p-3 pt-3">
<SkeletonBar className="h-3 w-3 rounded-sm bg-[#0070F3]" />
<div className="flex flex-col gap-1.5 mt-3">
<SkeletonBar className="h-2 w-full bg-muted" />
<SkeletonBar className="h-2 w-14" />
<SkeletonBar className="h-2 w-full" />
<SkeletonBar className="h-2 w-10" />
</div>
</div>
{/* Main */}
<div className="flex-1 flex flex-col gap-2 p-3">
<SkeletonBar className="h-2.5 w-16 bg-muted" />
{/* 3x4 data grid */}
<div className="grid grid-cols-4 grid-rows-3 gap-1.5 flex-1">
{Array.from({ length: 12 }).map((_, i) => (
<div
key={i}
className={cn(
'rounded-sm bg-foreground-muted/10 min-h-[40px]',
[1, 6, 9].includes(i) && 'bg-muted/70'
)}
/>
))}
</div>
</div>
</div>
)
}
const SKELETON_CONTENT: Record<string, () => React.JSX.Element> = {
'Stripe Subscriptions Starter': StripeSkeleton,
'Next.js Starter': NextjsSkeleton,
}
export function BuiltWithSupabaseSection() {
const featuredExamples = Examples.slice(0, 2)
const gridExamples = Examples.slice(2, 6)
return (
<div className="py-24 flex flex-col gap-16">
{/* Header row */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] w-full px-6">
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<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>
</h3>
<Link
href="/docs/guides/examples"
className="text-sm text-foreground-light hover:text-foreground underline"
>
View all examples
</Link>
</div>
</div>
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
{/* Featured row - 2 large cells */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{featuredExamples.map((example) => {
const logo = EXAMPLE_LOGO[example.title]
const Skeleton = SKELETON_CONTENT[example.title]
return (
<Link
key={example.title}
href={example.repo_url ?? '#'}
target="_blank"
rel="noopener noreferrer"
className="group flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden hover:bg-surface-100 transition-colors"
>
<div className="px-6 py-5 flex flex-col gap-2.5">
{logo && (
<img
src={logo.src}
alt=""
className={`h-5 w-fit object-contain ${logo.invert ? ' dark:invert' : ''}`}
/>
)}
<div>
<h4 className="text-foreground text-sm font-medium">{example.title}</h4>
<p className="text-foreground-lighter text-sm mt-1 line-clamp-2">
{example.description}
</p>
</div>
</div>
<div className="flex-1 flex items-center justify-center px-6 pt-4 pb-0 overflow-hidden">
{/* Browser frame */}
<div className="w-full rounded-t-lg border border-b-0 border-border-stronger/50 shadow-lg -mb-[10%] overflow-hidden">
{/* Title bar */}
<div className="flex items-center gap-1.5 px-3 py-2.5 border-b border-border bg-background-200">
<span className="size-2.5 rounded-full bg-foreground-muted/30" />
<span className="size-2.5 rounded-full bg-foreground-muted/30" />
<span className="size-2.5 rounded-full bg-foreground-muted/30" />
</div>
{/* Skeleton content */}
<div className="h-[240px] flex items-center justify-center bg-background">
{Skeleton ? <Skeleton /> : null}
</div>
</div>
</div>
</Link>
)
})}
</div>
{/* Grid row - 4 smaller cells */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 mt-3">
{gridExamples.map((example) => {
const logo = EXAMPLE_LOGO[example.title]
return (
<Link
key={example.title}
href={example.repo_url ?? '#'}
target="_blank"
rel="noopener noreferrer"
className="group flex flex-col bg-surface-75 border border-border rounded-lg px-6 py-10 hover:bg-surface-100 transition-colors"
>
<div className="flex items-center gap-2 mb-3">
{logo && (
<img
src={logo.src}
alt=""
className={`h-5 w-5 object-contain${logo.invert ? ' dark:invert' : ''}`}
/>
)}
</div>
<h4 className="text-foreground text-sm font-medium">{example.title}</h4>
<p className="text-foreground-lighter text-sm mt-1 line-clamp-2">
{example.description}
</p>
</Link>
)
})}
</div>
</div>
</div>
)
}
@@ -0,0 +1,193 @@
'use client'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { useInView } from 'framer-motion'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { Button } from 'ui'
interface GitHubContributor {
id: number
login: string
avatar_url: string
contributions: number
}
interface AvatarPosition {
id: number
login: string
avatar_url: string
x: number
y: number
size: number
opacity: number
delay: number
}
function useContributors() {
const [contributors, setContributors] = useState<GitHubContributor[]>([])
useEffect(() => {
let cancelled = false
async function fetchAll() {
try {
const pages = await Promise.all(
[1, 2].map((page) =>
fetch(
`https://api.github.com/repos/supabase/supabase/contributors?per_page=100&page=${page}`,
{ headers: { Accept: 'application/vnd.github.v3+json' } }
).then((r) => (r.ok ? r.json() : []))
)
)
if (!cancelled) setContributors(pages.flat())
} catch {
// silent fail
}
}
fetchAll()
return () => {
cancelled = true
}
}, [])
return contributors
}
function AvatarCloud({ contributors }: { contributors: GitHubContributor[] }) {
const containerRef = useRef<HTMLDivElement>(null)
const inView = useInView(containerRef, { once: true, amount: 0.4 })
const [positions, setPositions] = useState<AvatarPosition[]>([])
useEffect(() => {
if (contributors.length === 0 || !containerRef.current) return
const generate = () => {
const width = containerRef.current!.clientWidth
const avatarSize = 28
const gap = 6
const cols = Math.ceil(width / (avatarSize + gap))
const rows = 10
const centerX = cols / 2
const centerY = -3
const radiusX = cols * 0.35
const radiusY = rows * 0.8
const result: AvatarPosition[] = []
let ci = 0
for (let row = 0; row < rows; row++) {
const rowOffset = row % 2 === 0 ? 0 : (avatarSize + gap) / 2
for (let col = 0; col < cols; col++) {
const dx = (col - centerX) / radiusX
const dy = (row - centerY) / radiusY
if (dx * dx + dy * dy < 1) continue
if (Math.random() > 0.82) continue
const c = contributors[ci % contributors.length]
ci++
const edgeDistX = Math.min(col, cols - col - 1) / (cols * 0.12)
const edgeDistY = row / (rows * 0.3)
const edgeFade = Math.min(1, edgeDistX, edgeDistY)
result.push({
id: c.id,
login: c.login,
avatar_url: c.avatar_url,
x: col * (avatarSize + gap) + rowOffset + (Math.random() - 0.5) * 6,
y: row * (avatarSize + gap) + (Math.random() - 0.5) * 6,
size: 24 + Math.random() * 8,
opacity: Math.min(0.85, edgeFade * (0.35 + Math.random() * 0.5)),
delay: Math.random() * 2,
})
}
}
setPositions(result)
}
generate()
const onResize = () => generate()
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [contributors])
return (
<div ref={containerRef} className="absolute inset-x-0 bottom-0 h-[340px] overflow-hidden">
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-background via-transparent to-transparent z-10" />
<div className="pointer-events-none absolute inset-0 bg-gradient-to-r from-background via-transparent to-background z-10" />
<div className="relative w-full h-full">
{positions.map((pos, i) => (
<div
key={`${pos.id}-${i}`}
className="absolute transition-opacity duration-1000 ease-out"
style={{
left: pos.x,
top: pos.y,
width: pos.size,
height: pos.size,
opacity: inView ? pos.opacity : 0,
transitionDelay: `${pos.delay}s`,
}}
>
<img
src={pos.avatar_url}
alt={pos.login}
width={pos.size}
height={pos.size}
className="rounded-md border border-foreground/10 grayscale pointer-events-none"
loading="lazy"
/>
</div>
))}
</div>
</div>
)
}
export function CTASection() {
const sendTelemetryEvent = useSendTelemetryEvent()
const contributors = useContributors()
return (
<div className="relative overflow-hidden py-32">
{/*<AvatarCloud contributors={contributors} />*/}
<div className="relative z-20 flex flex-col items-center text-center gap-6">
<h2 className="h2">
<span className="text-foreground-lighter">Build in a weekend,</span>
<span className="text-foreground block sm:inline"> scale to millions</span>
</h2>
<div className="flex items-center gap-2">
<Button asChild size="medium">
<Link
href="https://supabase.com/dashboard"
onClick={() =>
sendTelemetryEvent({
action: 'start_project_button_clicked',
properties: { buttonLocation: 'CTA Banner' },
})
}
>
Start your project
</Link>
</Button>
<Button asChild size="medium" type="default">
<Link
href="/contact/sales"
onClick={() =>
sendTelemetryEvent({
action: 'request_demo_button_clicked',
properties: { buttonLocation: 'CTA Banner' },
})
}
>
Request a demo
</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,72 @@
'use client'
import Link from 'next/link'
import { topTweets } from 'shared-data/tweets'
import { Button } from 'ui'
import { TweetCard } from 'ui-patterns/TweetCard'
const STAIRCASE_COLUMNS = 5
const TWEETS_PER_COL = 3
const STAIRCASE_OFFSET = 48
export function CommunitySection() {
const tweets = topTweets.slice(0, STAIRCASE_COLUMNS * TWEETS_PER_COL)
const columns: (typeof tweets)[] = Array.from({ length: STAIRCASE_COLUMNS }, (_, i) =>
tweets.slice(i * TWEETS_PER_COL, (i + 1) * TWEETS_PER_COL)
)
return (
<div className="border-b border-border">
<div className="pt-16 px-6">
<div className="flex flex-col items-center gap-3 mb-12">
<h3 className="text-2xl md:text-4xl text-center text-foreground-lighter">
Join the community
</h3>
<p className="text-foreground-lighter text-center">
Discover what our community has to say about their Supabase experience.
</p>
<Button asChild type="default" size="small" className="mt-2">
<Link href="https://discord.supabase.com/" target="_blank">
Join us on Discord
</Link>
</Button>
</div>
<div
className="relative overflow-hidden max-h-[600px] pt-24"
style={{
maskImage: 'linear-gradient(to bottom, black 60%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 60%, transparent 100%)',
}}
>
<div className="flex gap-4 max-w-[1480px] mx-auto">
{columns.map((col, colIdx) => (
<div
key={colIdx}
className="flex-1 flex flex-col gap-4 min-w-0"
style={{
transform: `translateY(-${Math.abs(colIdx - Math.floor(STAIRCASE_COLUMNS / 2)) * STAIRCASE_OFFSET}px)`,
}}
>
{col.map((tweet) => (
<Link
key={tweet.url}
href={tweet.url}
target="_blank"
className="block group/tweet-card break-inside-avoid"
>
<TweetCard
handle={`@${tweet.handle}`}
quote={tweet.text}
img_url={tweet.img_url}
/>
</Link>
))}
</div>
))}
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,105 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { topTweets } from 'shared-data/tweets'
import { Button } from 'ui'
import { TweetCard } from 'ui-patterns/TweetCard'
const COLS = 5
const TWEETS_PER_PAGE = 15
// V-shape offsets: outer columns high, center lowest
const COL_OFFSETS = [0, 60, 100, 60, 0]
function distributeToColumns(tweets: typeof topTweets, cols: number) {
const columns: (typeof topTweets)[] = Array.from({ length: cols }, () => [])
tweets.forEach((tweet, i) => columns[i % cols].push(tweet))
return columns
}
export function CommunitySectionV2() {
const [page, setPage] = useState(0)
useEffect(() => {
if (topTweets.length === 0) return
const interval = setInterval(() => {
setPage((p) => p + 1)
}, 10000)
return () => clearInterval(interval)
}, [])
if (topTweets.length === 0) return null
const currentTweets = Array.from({ length: TWEETS_PER_PAGE }, (_, i) => {
const idx = (page * TWEETS_PER_PAGE + i) % topTweets.length
return topTweets[idx]
})
const columns = distributeToColumns(currentTweets, COLS)
return (
<div className="border-b border-border">
<div className="mx-auto">
<div className="flex flex-col items-center gap-3 px-6 py-20">
<h3 className="text-2xl md:text-4xl text-center">Join the community</h3>
<p className="text-foreground-lighter text-center text-sm">
Discover what our community has to say about their Supabase experience.
</p>
<Button asChild type="default" size="small" className="mt-2">
<Link href="https://discord.supabase.com/" target="_blank">
Join us on Discord
</Link>
</Button>
</div>
<div
className="relative overflow-hidden h-[620px]"
style={{
maskImage:
'linear-gradient(to bottom, black 50%, transparent 100%), linear-gradient(to right, transparent, black 10%, black 90%, transparent)',
WebkitMaskImage:
'linear-gradient(to bottom, black 50%, transparent 100%), linear-gradient(to right, transparent, black 10%, black 90%, transparent)',
maskComposite: 'intersect',
WebkitMaskComposite: 'destination-in',
}}
>
<AnimatePresence mode="popLayout" initial={false}>
<motion.div key={page} className="grid grid-cols-2 md:grid-cols-5 gap-2 -mx-4">
{columns.map((col, colIdx) => (
<div
key={colIdx}
className="flex flex-col gap-2"
style={{ transform: `translateY(${COL_OFFSETS[colIdx]}px)` }}
>
{col.map((tweet, i) => (
<motion.div
key={tweet.url}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{
duration: 0.45,
delay: (colIdx + i * COLS) * 0.03,
ease: [0.25, 0.46, 0.45, 0.94],
}}
>
<Link href={tweet.url} target="_blank" className="block group/tweet-card">
<TweetCard
handle={`@${tweet.handle}`}
quote={tweet.text}
img_url={tweet.img_url}
/>
</Link>
</motion.div>
))}
</div>
))}
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
)
}
@@ -0,0 +1,419 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { useState } from 'react'
import { Button, cn } from 'ui'
const customerStories = [
{
name: 'Firecrawl',
logo: '/images/customers/logos/firecrawl.png',
icon: '/images/customers/logos/firecrawl-icon.svg',
tagline: 'Switched from Pinecone to Supabase Vector to boost efficiency and accuracy.',
quote:
"We looked at the alternatives and chose Supabase because it's open source, it's simpler, and for all the ways we need to use it, Supabase has been just as performant — if not more performant — than the other vector databases.",
author: 'Caleb Peffer, CEO, Firecrawl',
authorImg: '/images/blog/avatars/caleb-peffer.jpg',
slug: 'firecrawl',
rawIcon: true,
iconFilter: 'brightness(0) invert(1)',
bgColor: 'color(display-p3 0.980392 0.364706 0.098039)',
bgGradient:
'linear-gradient(to bottom left, color(display-p3 0.980392 0.364706 0.098039 / 1) 0%, color(display-p3 0.980392 0.364706 0.098039 / 0.9) 100%)',
dimBgColor: 'color(display-p3 0.980392 0.364706 0.098039 / 0.9)',
textColor: 'light' as 'light' | 'dark',
},
{
name: 'Rally',
logo: '/images/customers/logos/rally.png',
icon: '/images/customers/logos/rally-icon.svg',
rawIcon: true,
tagline: 'From first line of code to fully licensed fintech in three months.',
quote:
"We could not have built this company without Supabase. If I had to go and build all these components myself, we wouldn't even have launched.",
author: 'Thiago Peres, Founder & CTO, Rally',
authorImg: '/images/blog/avatars/thiago-peres-rally.jpeg',
slug: 'rally',
bgColor: 'color(display-p3 0.275 0.306 0.8)',
bgGradient:
'linear-gradient(to bottom left, color(display-p3 0.275 0.306 0.8 / 1) 0%, color(display-p3 0.118 0.176 0.769 / 1) 100%)',
dimBgColor: 'color(display-p3 0.118 0.176 0.769 / 1)',
textColor: 'light' as 'light' | 'dark',
},
{
name: 'Hyper',
logo: '/images/customers/logos/hyper.svg',
icon: '/images/customers/logos/hyper-icon.svg',
tagline: 'An AI-native marketing platform with agents that operate across the entire workflow.',
quote:
'I will get on a podcast and talk about how much I love Supabase. With Supabase we can move fast and build things that delight our customers without having to worry about infrastructure.',
author: 'Elliot Fleck, Co-founder, Hyper',
authorImg: '/images/blog/avatars/elliot-fleck-hyper.jpeg',
slug: 'hyper',
bgColor: '#222222',
bgGradient: 'linear-gradient(to bottom left, #2a2a2a 0%, #181818 100%)',
dimBgColor: '#1e1e1e',
textColor: 'light' as 'light' | 'dark',
},
{
name: 'E2B',
logo: '/images/customers/logos/e2b.png',
icon: '/images/customers/logos/e2b-icon.svg',
tagline: 'Secure, scalable execution of AI-generated code in the cloud.',
quote:
"Supabase empowers us to focus on innovation rather than infrastructure. It's the backbone of our platform, enabling scalability and seamless developer experiences.",
author: 'Vasek Mlejnsky, CEO, E2B',
authorImg: '/images/blog/avatars/vasek-mlejnsky.jpg',
slug: 'e2b',
rawIcon: true,
iconFilter: 'brightness(0) invert(1)',
bgColor: 'color(display-p3 1 0.533 0)',
bgGradient:
'linear-gradient(to bottom left, color(display-p3 1 0.533 0 / 1) 0%, color(display-p3 0.7 0.373 0 / 1) 100%)',
dimBgColor: 'color(display-p3 0.7 0.373 0 / 1)',
textColor: 'light' as 'light' | 'dark',
},
{
name: 'Mobbin',
logo: '/images/customers/logos/mobbin.png',
icon: '/images/customers/logos/mobbin-icon.svg',
tagline: 'Migrated 200,000 users from Firebase for a better authentication experience.',
quote:
'Migrating to Supabase meant that we could instantly fix our Auth problems and save money. Just being on Supabase alone gives us confidence we can deliver on whatever users need in the future.',
author: 'Jian Jie Liau, Co-founder & CTO, Mobbin',
authorImg: '/images/blog/avatars/jian-mobbin.jpg',
slug: 'mobbin',
bgColor: '#000000',
bgGradient: 'linear-gradient(to bottom left, #0a0a0a 0%, #000000 100%)',
dimBgColor: '#080808',
textColor: 'light' as 'light' | 'dark',
},
]
const aiBuilderStories = [
{
name: 'Lovable',
logo: '/images/logos/publicity/lovable.svg',
description: 'We store embeddings in a PostgreSQL database.',
slug: 'lovable',
photo: '/images/logos/publicity/lovable-team.png',
},
{
name: 'Bolt',
logo: '/images/logos/publicity/bolt.svg',
description: 'We store embeddings in a PostgreSQL database, hosted by Supabase.',
slug: 'bolt',
photo: '/images/logos/publicity/bolt-team.png',
},
]
function LovableLogo({ className }: { className?: string }) {
return (
<svg viewBox="0 0 121 122" fill="none" className={className}>
<path
fill="white"
fillRule="evenodd"
d="M36.069 0c19.92 0 36.068 16.155 36.068 36.084v13.713h12.004c19.92 0 36.069 16.156 36.069 36.084 0 19.928-16.149 36.083-36.069 36.083H0v-85.88C0 16.155 16.148 0 36.069 0Z"
clipRule="evenodd"
/>
</svg>
)
}
function IconChip({
story,
size = 'md',
}: {
story: (typeof customerStories)[0]
size?: 'sm' | 'md'
}) {
const isLight = story.textColor === 'light'
const s = story as any
const filter =
s.iconFilter ?? (s.rawIcon ? undefined : isLight ? 'brightness(0) invert(1)' : 'brightness(0)')
return (
<img
src={story.icon}
alt={story.name}
className={cn('object-contain shrink-0', size === 'md' ? 'h-8 w-8' : 'h-6 w-6')}
style={filter ? { filter } : undefined}
/>
)
}
// Change INACTIVE_PAD to adjust closed column width automatically
const INACTIVE_PAD = 22 // px padding on each side of icon in closed columns
const ACTIVE_PAD = 32 // px padding on each side when card is open
const ICON_PX = 32 // w-8 = 32px
const INACTIVE_COL_WIDTH = INACTIVE_PAD * 2 + ICON_PX // = 76px
export function CustomerStoriesSection() {
const [activeIdx, setActiveIdx] = useState(0)
const active = customerStories[activeIdx]
return (
<div className="py-24 flex flex-col gap-16">
{/* Header row */}
<div className="">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<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>
</h3>
<Link
href="/customers"
className="text-sm text-foreground-light hover:text-foreground underline"
>
More customer stories
</Link>
</div>
</div>
</div>
{/* Cards row */}
<div className="px-6 mx-auto max-w-[var(--container-max-w,75rem)] w-full">
{/* Mobile: stacked cards */}
<div className="flex flex-col gap-2 md:hidden">
{customerStories.map((story, index) => {
const isActive = index === activeIdx
const isDark = story.textColor === 'dark'
return (
<div
key={story.slug}
role="button"
tabIndex={0}
onClick={() => setActiveIdx(index)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveIdx(index)
}
}}
className="text-left rounded-lg p-5 flex flex-col gap-4 overflow-hidden transition-opacity cursor-pointer"
style={{ background: isActive ? story.bgGradient : story.dimBgColor }}
>
<IconChip story={story} size="sm" />
{isActive && (
<div className="flex flex-col gap-3 flex-1">
<div>
<p
className="text-sm font-medium"
style={{ color: isDark ? '#111' : 'white' }}
>
{story.name}
</p>
<p
className="text-xs mt-0.5"
style={{ color: isDark ? '#888' : 'rgba(255,255,255,0.6)' }}
>
{story.tagline}
</p>
</div>
<p
className="text-xl font-normal leading-snug text-pretty"
style={{
color: isDark ? '#222' : 'white',
fontFamily: 'var(--font-heading)',
}}
>
{story.quote}
</p>
<div className="flex items-center gap-2 mb-4">
<img
src={story.authorImg}
alt={story.author}
className="h-6 w-6 rounded-full object-cover shrink-0 ring-1 ring-white/30"
/>
<p
className="text-xs"
style={{ color: isDark ? '#888' : 'rgba(255,255,255,0.65)' }}
>
{story.author}
</p>
</div>
<Link
href={`/customers/${story.slug}`}
className="text-xs underline"
style={{ color: isDark ? '#555' : 'rgba(255,255,255,0.7)' }}
>
Read the story →
</Link>
</div>
)}
</div>
)
})}
</div>
{/* Desktop: animated accordion grid */}
<div
className="hidden md:grid min-h-[480px] gap-2"
style={{
gridTemplateColumns: customerStories
.map((_, i) => (i === activeIdx ? '1fr' : `${INACTIVE_COL_WIDTH}px`))
.join(' '),
}}
>
{customerStories.map((story, index) => {
const isActive = index === activeIdx
const isDark = story.textColor === 'dark'
return (
<motion.div
layout
key={story.slug}
role="button"
tabIndex={0}
onClick={() => setActiveIdx(index)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setActiveIdx(index)
}
}}
className="text-left flex flex-col items-start gap-8 overflow-hidden cursor-pointer"
style={{
background: story.bgGradient,
borderRadius: 8,
padding: isActive ? ACTIVE_PAD : `${ACTIVE_PAD}px ${INACTIVE_PAD}px`,
boxShadow: isDark
? 'inset 0 0 0 1px rgba(0,0,0,0.07)'
: 'inset 0 0 0 1px rgba(255,255,255,0.12)',
}}
transition={{ type: 'spring', duration: 0.4, bounce: 0 }}
>
<motion.div layout>
<IconChip story={story} />
</motion.div>
<motion.div
layout
className="flex flex-col gap-1.5 flex-1 w-[35rem]"
initial={false}
animate={{
opacity: isActive ? 1 : 0,
filter: isActive ? 'blur(0px)' : 'blur(2px)',
}}
transition={{
duration: 0.42,
ease: [0.165, 0.84, 0.44, 1],
delay: isActive ? 0.2 : 0,
}}
>
{/* Top: company name + tagline */}
<motion.div layout className="flex flex-col gap-1">
<motion.p
layout
className="text-sm font-medium"
style={{ color: isDark ? '#111' : 'white' }}
>
{story.name}
</motion.p>
<motion.p
layout
className="text-xs leading-relaxed text-pretty"
style={{ color: isDark ? '#888' : 'rgba(255,255,255,0.6)' }}
>
{story.tagline}
</motion.p>
</motion.div>
{/* Bottom: quote + author */}
<motion.div layout className="flex flex-col gap-4 mt-auto">
<motion.p
layout
className="text-2xl font-normal leading-snug text-balance"
style={{
color: isDark ? '#222' : 'white',
fontFamily: 'var(--font-heading)',
}}
>
{story.quote}
</motion.p>
<motion.div layout className="flex items-center gap-2.5 mb-4">
<img
src={story.authorImg}
alt={story.author}
className="h-7 w-7 rounded-full object-cover shrink-0 ring-1 ring-white/30"
/>
<motion.p
layout
className="text-xs"
style={{ color: isDark ? '#888' : 'rgba(255,255,255,0.65)' }}
>
{story.author}
</motion.p>
</motion.div>
<Link
href={`/customers/${story.slug}`}
className="text-xs underline"
style={{ color: isDark ? '#555' : 'rgba(255,255,255,0.7)' }}
>
Read the story →
</Link>
</motion.div>
</motion.div>
</motion.div>
)
})}
</div>
</div>
{/* AI Builder stories */}
<div className="mt-10">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="grid grid-cols-1 items-end lg:grid-cols-[420px_1fr] gap-8">
<div className="flex flex-col md:min-h-[320px] gap-4 justify-between">
<h3 className="text-xl md:text-3xl text-foreground-lighter">
Powering the next wave
<br />
<span className="text-foreground">of AI builders</span>
</h3>
<p className="text-sm text-foreground-lighter leading-relaxed text-pretty max-w-sm">
Create full backend experiences on behalf of your users with Supabase for Platforms,
the all-in-one solution for building AI-native platforms and marketplaces.
</p>
<Button type="default" size="medium" asChild className="w-fit mt-auto">
<Link href="/partners/integrations">Explore Supabase for Platforms</Link>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{aiBuilderStories.map((story) => (
<Link
key={story.slug}
href={`/customers/${story.slug}`}
className="group relative overflow-hidden rounded-lg w-full aspect-[4/3] border border-white/10 flex flex-col justify-end p-5"
>
{/* Photo background */}
<img
src={story.photo}
alt={story.name}
className="absolute inset-0 w-full h-full object-cover select-none scale-[1.015]"
/>
{/* Dark gradient overlay — heavier at bottom */}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent" />
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-[background-color] duration-300" />
{/* Logo + description at bottom */}
<div className="relative z-10 flex flex-col gap-2">
<img
src={story.logo}
alt={story.name}
className="h-6 w-auto object-contain object-left brightness-0 invert select-none"
/>
<p className="text-white/75 text-sm leading-relaxed text-pretty">
{story.description}
</p>
</div>
</Link>
))}
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,145 @@
'use client'
import BrowserFrame from '~/components/BrowserFrame'
import { AnimatePresence, motion, useInView } from 'framer-motion'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import React, { useRef, useState } from 'react'
import { cn } from 'ui'
const dashboardStories = [
{
logo: '/images/customers/logos/hyper-icon.svg',
name: 'Hyper',
quote:
'We store embeddings in a PostgreSQL database, hosted by Supabase, to perform a similarity search to identify the most relevant sections within the MDN.',
author: 'Hermina Condei, Director at MDN, Mozilla',
slug: 'mozilla-mdn',
},
{
logo: '/images/customers/logos/chatbase-icon.svg',
name: 'Chatbase',
quote:
'The SQL Editor in Supabase is my favorite feature — it lets us run complex queries, debug issues, and iterate on our schema without ever leaving the browser.',
author: 'Yasser Elsaid, Founder of Chatbase',
slug: 'chatbase',
},
{
logo: '/images/customers/logos/markprompt-icon.svg',
name: 'Markprompt',
quote:
'Row Level Security in Supabase allows us to set granular access policies directly in Postgres, so our multi-tenant architecture stays secure by default.',
author: 'Michael Fester, Co-Founder of Markprompt',
slug: 'markprompt',
},
]
export function DashboardFeaturesSection({
title,
tabs,
}: {
title: React.ReactNode
tabs: {
label: string
panel: React.FC<{ isDark: boolean }>
highlights: { label: string; link?: string }[]
}[]
}) {
const { resolvedTheme } = useTheme()
const [activeTabIdx, setActiveTabIdx] = useState(0)
const sectionRef = useRef(null)
const isInView = useInView(sectionRef, { once: true })
if (tabs.length === 0) return null
const Panel = tabs[activeTabIdx].panel
const story = dashboardStories[activeTabIdx] ?? dashboardStories[0]
return (
<div className="border-b border-border">
<div
ref={sectionRef}
className="relative mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-10 pb-4 overflow-hidden"
>
<div className="pt-12 flex flex-col gap-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter">{title}</h3>
<div className="flex gap-2" role="tablist">
{tabs.map((tab, index) => (
<button
key={index}
onClick={() => setActiveTabIdx(index)}
aria-selected={index === activeTabIdx}
role="tab"
className={cn(
'py-1.5 px-3 lg:py-2 lg:px-8 border rounded-full bg-alternative hover:border-foreground text-sm opacity-80 transition-all',
'hover:border-foreground-lighter hover:text-foreground',
index === activeTabIdx ? 'opacity-100 !border-foreground' : ''
)}
>
{tab.label}
</button>
))}
</div>
</div>
<BrowserFrame
className="overflow-hidden bg-default w-full mx-auto translate-y-16"
contentClassName="aspect-video overflow-hidden rounded-lg"
>
{isInView && (
<AnimatePresence mode="wait">
<motion.div
key={tabs[activeTabIdx]?.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.1, delay: 0.2 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="relative w-full max-w-full h-full"
>
<Panel
key={String(resolvedTheme?.includes('dark'))}
isDark={resolvedTheme?.includes('dark') ?? false}
/>
</motion.div>
</AnimatePresence>
)}
</BrowserFrame>
</div>
{/* Customer story banner */}
<div className="border-t border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] py-4">
<AnimatePresence mode="wait">
<motion.div
key={story.slug}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="flex flex-col md:flex-row md:items-center gap-4 md:gap-6 px-6 py-5"
>
<img
src={story.logo}
alt={story.name}
className="h-8 w-8 shrink-0 object-contain grayscale opacity-80 dark:invert"
/>
<div className="flex-1 min-w-0">
<p className="text-foreground-lighter text-sm">
<span className="font-medium text-foreground">{story.name}</span>
{' — '}
{story.quote} <span className="text-foreground-muted">{story.author}</span>
</p>
</div>
<Link
href={`/customers/${story.slug}`}
className="shrink-0 text-sm text-foreground-lighter hover:text-foreground transition-colors whitespace-nowrap"
>
View more about {story.name}
</Link>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
)
}
@@ -0,0 +1,350 @@
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { FrameworksSectionClient } from './FrameworksSectionClient'
// ── Shiki themes ────────────────────────────────────────────────────────────
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#bda4ff' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#3ecf8e' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#3ecf8e' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#3ecf8e' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#ffffff' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
{ scope: ['markup.underline.link'], settings: { foreground: '#ffffff' } },
{ scope: ['markup.inserted'], settings: { foreground: '#3ecf8e' } },
{ scope: ['markup.deleted'], settings: { foreground: '#F06A50' } },
],
}
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#6b35dc' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#15593b' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#15593b' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#15593b' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#525252' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
{ scope: ['markup.underline.link'], settings: { foreground: '#525252' } },
],
}
// ── Framework data ──────────────────────────────────────────────────────────
const frameworksList: {
name: string
icon: string
code: string
lang: 'javascript' | 'dart' | 'svelte' | 'vue'
docsUrl: string
}[] = [
{
name: 'React',
icon: 'M45.74 23.6983C45.2739 23.5379 44.7909 23.3861 44.2937 23.2426C44.3754 22.909 44.4504 22.5798 44.5171 22.2561C45.6119 16.9418 44.8961 12.6605 42.4518 11.2509C40.1079 9.89927 36.2748 11.3085 32.4035 14.6776C32.0313 15.0016 31.6579 15.3446 31.2848 15.704C31.0362 15.4662 30.7879 15.2364 30.5403 15.0165C26.4831 11.4141 22.4164 9.89599 19.9744 11.3097C17.6329 12.6652 16.9394 16.69 17.9249 21.7265C18.0201 22.2129 18.1313 22.7097 18.2571 23.2148C17.6816 23.3782 17.1259 23.5524 16.5943 23.7377C11.8376 25.3961 8.7998 27.9952 8.7998 30.6911C8.7998 33.4755 12.0609 36.2683 17.0153 37.9617C17.4063 38.0953 17.812 38.2217 18.2301 38.3416C18.0944 38.8879 17.9763 39.4232 17.8773 39.9454C16.9376 44.8944 17.6714 48.8242 20.0068 50.1711C22.4189 51.5622 26.4673 50.1324 30.4093 46.6865C30.7209 46.4141 31.0336 46.1253 31.3469 45.8225C31.7529 46.2135 32.1582 46.5835 32.5615 46.9306C36.3798 50.2164 40.151 51.5432 42.4842 50.1925C44.894 48.7975 45.6772 44.576 44.6604 39.4399C44.5828 39.0476 44.4924 38.6469 44.3909 38.239C44.6752 38.155 44.9543 38.0682 45.2265 37.978C50.3771 36.2715 53.7282 33.5127 53.7282 30.6911C53.7282 27.9854 50.5924 25.3688 45.74 23.6983ZM44.6228 36.1561C44.3772 36.2375 44.1251 36.3161 43.8682 36.3923C43.2996 34.5922 42.5322 32.6781 41.5931 30.7005C42.4893 28.7699 43.227 26.8803 43.7797 25.0919C44.2393 25.2249 44.6854 25.3651 45.1152 25.5132C49.2728 26.9444 51.8089 29.0605 51.8089 30.6911C51.8089 32.4279 49.07 34.6826 44.6228 36.1561ZM42.7776 39.8126C43.2272 42.0837 43.2914 44.1371 42.9936 45.7423C42.726 47.1847 42.1878 48.1463 41.5225 48.5315C40.1066 49.351 37.0787 48.2857 33.8132 45.4757C33.4388 45.1535 33.0618 44.8096 32.6835 44.4455C33.9495 43.061 35.2147 41.4514 36.4495 39.6638C38.6215 39.4711 40.6735 39.156 42.5344 38.7258C42.626 39.0955 42.7074 39.4581 42.7776 39.8126ZM24.1169 48.3898C22.7336 48.8784 21.6318 48.8924 20.9658 48.5084C19.5486 47.691 18.9594 44.5358 19.7631 40.3033C19.8551 39.8186 19.9647 39.3207 20.091 38.8118C21.9314 39.2187 23.9684 39.5116 26.1456 39.6881C27.3887 41.4373 28.6905 43.0452 30.0024 44.453C29.7157 44.7297 29.4302 44.9931 29.1463 45.2413C27.4032 46.7651 25.6564 47.8461 24.1169 48.3898ZM17.6361 36.1455C15.4453 35.3967 13.6361 34.4235 12.396 33.3616C11.2817 32.4073 10.7191 31.4599 10.7191 30.6911C10.7191 29.0551 13.1581 26.9684 17.226 25.5501C17.7196 25.378 18.2363 25.2158 18.7725 25.0635C19.3347 26.8923 20.0722 28.8043 20.9623 30.7378C20.0607 32.7 19.3128 34.6425 18.745 36.4927C18.3628 36.3829 17.9924 36.2672 17.6361 36.1455ZM19.8085 21.3579C18.9642 17.0428 19.5249 13.7876 20.936 12.9708C22.4391 12.1006 25.7628 13.3413 29.2659 16.4518C29.4898 16.6506 29.7146 16.8587 29.9401 17.074C28.6347 18.4756 27.3448 20.0714 26.1127 21.8103C23.9997 22.0061 21.977 22.3208 20.1174 22.742C20.0004 22.2717 19.8969 21.8097 19.8085 21.3579ZM39.1886 26.1433C38.744 25.3754 38.2876 24.6257 37.8223 23.8964C39.2558 24.0777 40.6293 24.3182 41.9191 24.6126C41.5318 25.8536 41.0492 27.1511 40.4811 28.4813C40.0735 27.7076 39.6425 26.9275 39.1886 26.1433ZM31.2854 18.4456C32.1707 19.4047 33.0573 20.4756 33.9293 21.6374C33.0506 21.5959 32.161 21.5743 31.264 21.5743C30.3755 21.5743 29.4925 21.5954 28.6192 21.6362C29.4921 20.4852 30.3863 19.4158 31.2854 18.4456ZM23.3317 26.1566C22.8876 26.9267 22.4645 27.7025 22.0634 28.4799C21.5045 27.1543 21.0263 25.8509 20.6357 24.5923C21.9176 24.3054 23.2846 24.0709 24.7089 23.8931C24.2371 24.6291 23.7769 25.3843 23.3317 26.1564V26.1566ZM24.75 37.626C23.2783 37.4618 21.8908 37.2394 20.6093 36.9604C21.0061 35.6793 21.4948 34.3481 22.0655 32.994C22.4677 33.7707 22.8925 34.5469 23.3393 35.3187H23.3393C23.7945 36.1049 24.266 36.875 24.75 37.626ZM31.3385 43.0719C30.4289 42.0904 29.5215 41.0047 28.6353 39.8368C29.4956 39.8706 30.3726 39.8879 31.264 39.8879C32.1798 39.8879 33.085 39.8672 33.9761 39.8276C33.1012 41.0164 32.2178 42.1038 31.3385 43.0719ZM40.4994 32.9249C41.0999 34.2937 41.6061 35.618 42.0081 36.8772C40.7054 37.1744 39.2989 37.4138 37.8171 37.5916C38.2835 36.8525 38.7439 36.0899 39.1963 35.3055C39.6539 34.5118 40.0885 33.717 40.4994 32.9249ZM37.5337 34.3466C36.8314 35.5643 36.1104 36.7268 35.3784 37.8241C34.0452 37.9194 32.6678 37.9685 31.264 37.9685C29.8659 37.9685 28.5058 37.9251 27.1962 37.8401C26.4347 36.7284 25.698 35.5625 25.0002 34.3571H25.0004C24.3044 33.155 23.6638 31.9427 23.0834 30.7372C23.6636 29.5289 24.3025 28.3152 24.9945 27.1152L24.9944 27.1155C25.6882 25.9123 26.4184 24.7521 27.1729 23.6473C28.509 23.5463 29.8792 23.4936 31.2639 23.4936H31.264C32.655 23.4936 34.0269 23.5467 35.3626 23.6486C36.1056 24.7453 36.8308 25.9017 37.5274 27.1051C38.2319 28.3219 38.879 29.5275 39.4642 30.7099C38.8808 31.9126 38.2351 33.1303 37.5337 34.3466ZM41.4931 12.9137C42.9976 13.7813 43.5826 17.2804 42.6374 21.8688C42.5771 22.1615 42.5092 22.4597 42.4354 22.762C40.5715 22.3319 38.5474 22.0118 36.4282 21.813C35.1937 20.055 33.9143 18.4567 32.6302 17.0731C32.9755 16.741 33.3202 16.4243 33.6636 16.1254C36.9805 13.2388 40.0806 12.0991 41.4931 12.9137ZM31.264 26.6791C3Line truncated
code: `import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
)
export default function App() {
const [todos, setTodos] = useState([])
useEffect(() => {
supabase.from('todos').select('*')
.then(({ data }) => setTodos(data))
}, [])
return <TodoList items={todos} />
}`,
lang: 'javascript' as const,
docsUrl: '/docs/guides/getting-started/quickstarts/reactjs',
},
{
name: 'Next.js',
icon: 'M42.3148 48.6796C38.9009 50.9525 34.8014 52.2771 30.3924 52.2771C18.4957 52.2771 8.85156 42.6329 8.85156 30.7362C8.85156 18.8395 18.4957 9.19531 30.3924 9.19531C42.2891 9.19531 51.9333 18.8395 51.9333 30.7362C51.9333 37.1564 49.1245 42.9207 44.6688 46.8671L39.5552 40.2803V21.8278H36.584V36.4531L25.2299 21.8278H21.4808V39.6473H24.4801V25.6368L42.3148 48.6796Z',
code: `import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
export default async function Page() {
const { data: todos } = await supabase
.from('todos')
.select('*')
return <TodoList items={todos} />
}`,
lang: 'javascript' as const,
docsUrl: '/docs/guides/getting-started/quickstarts/nextjs',
},
{
name: 'Flutter',
icon: 'M46.5067 10.3828L34.3509 10.3962L14.75 29.9971L20.7974 36.0519L26.1125 30.7666L46.5067 10.3828Z M34.6996 28.4653C34.5272 28.4573 34.3493 28.4491 34.2378 28.5965L23.7856 39.0471L29.7894 45.0142L29.7825 45.021L34.079 49.3212C34.1072 49.3462 34.1352 49.3741 34.1637 49.4026C34.2813 49.5201 34.4074 49.6462 34.5895 49.6055C36.5743 49.601 38.5591 49.6017 40.544 49.6025C42.529 49.6032 44.5142 49.604 46.4998 49.5995L35.9333 39.0234L46.4963 28.467L34.906 28.464C34.8415 28.4719 34.7711 28.4686 34.6996 28.4653Z',
code: `import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
);
runApp(MyApp());
}
final supabase = Supabase.instance.client;
final data = await supabase
.from('todos')
.select('*');`,
lang: 'dart' as const,
docsUrl: '/docs/guides/getting-started/quickstarts/flutter',
},
{
name: 'Svelte',
icon: 'M29.9094 11.2292C35.956 7.37668 44.3187 9.17299 48.553 15.2334H48.5532C50.5831 18.0746 51.3826 21.614 50.771 25.0519C50.4778 26.677 49.8581 28.2259 48.9493 29.6047C50.2752 32.1335 50.7201 35.0322 50.2136 37.8422C49.6086 41.2154 47.6106 44.1777 44.7096 46.0024L34.0903 52.7707C28.0445 56.623 19.6818 54.8274 15.4466 48.7665C13.4171 45.9251 12.6176 42.3859 13.2288 38.948C13.5223 37.3227 14.1422 35.7738 15.0512 34.3949C13.7247 31.8665 13.2794 28.9677 13.786 26.1577C14.3913 22.7845 16.3893 19.8223 19.29 17.9974L29.9094 11.2292ZM19.8146 45.9861C21.8311 48.8931 25.4469 50.2333 28.8709 49.343H28.8708C29.6345 49.139 30.3624 48.8192 31.0293 48.3946L41.6512 41.6252C43.396 40.5274 44.5979 38.7455 44.9622 36.7164C45.33 34.6483 44.8489 32.5192 43.6278 30.8101C41.6113 27.9032 37.9955 26.5629 34.5715 27.4531C33.8084 27.6571 33.081 27.9768 32.4147 28.4012L28.3617 30.9842C28.1601 31.1125 27.9401 31.2092 27.7093 31.271C26.6776 31.5384 25.5887 31.1342 24.9815 30.2584C24.614 29.7429 24.4693 29.1012 24.5801 28.4779C24.6899 27.8669 25.0519 27.3302 25.5774 26.9996L36.2002 20.2298C36.4017 20.1015 36.6218 20.0048 36.8526 19.9431C37.8838 19.6754 38.9725 20.0795 39.5793 20.9551C39.9039 21.4146 40.0556 21.974 40.0078 22.5345L39.9714 22.9285L40.3662 23.0484C41.8596 23.4989 43.265 24.2014 44.5218 25.1254L45.0657 25.5245L45.2658 24.9145C45.3729 24.59 45.4577 24.2586 45.5196 23.9225C45.8873 21.8544 45.4063 19.7254 44.1852 18.0162C42.1687 15.1093 38.553 13.7691 35.129 14.6593C34.3653 14.8633 33.6374 15.1832 32.9705 15.6077L22.3487 22.3777C20.6036 23.475 19.4016 25.2568 19.0376 27.2858C18.6699 29.3539 19.1509 31.4829 20.372 33.192C22.3885 36.099 26.0043 37.4392 29.4283 36.549C30.1914 36.345 30.9188 36.0256 31.5853 35.6017L35.6389 33.0177C35.8402 32.8895 36.06 32.7929 36.2905 32.7311C37.3221 32.4637 38.4111 32.868 39.0183 33.7438C39.3857 34.2592 39.5306 34.9007 39.4205 35.524C39.3102 36.1352 38.948 36.6718 38.4224 37.0028L27.7996 43.7722C27.5981 43.9006 27.378 43.9973 27.1471 44.059C26.116 44.3266 25.0273 43.9225 24.4204 43.0469C24.0955 42.5876 23.9438 42.0281 23.992 41.4675L24.0284 41.0735L23.6336 40.9537C22.1404 40.5032 20.7351 39.8011 19.4783 38.8776L18.934 38.4778L18.734 39.0878C18.6266 39.4122 18.5418 39.7437 18.4801 40.0798C18.1125 42.1479 18.5935 44.277 19.8146 45.9861Z',
code: `<script>
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
PUBLIC_SUPABASE_URL,
PUBLIC_SUPABASE_ANON_KEY
)
let todos = []
async function loadTodos() {
const { data } = await supabase
.from('todos').select('*')
todos = data
}
</script>`,
lang: 'svelte' as const,
docsUrl: '/docs/guides/getting-started/quickstarts/sveltekit',
},
{
name: 'Vue',
icon: 'M43.0532 13.4531H50.1147L30.2756 47.8158L10.4365 13.4531H17.4978L30.2755 35.5845L43.0532 13.4531ZM42.1764 13.4531L30.2755 34.0659L18.3746 13.4531L25.6939 13.4531L30.2756 21.3888L34.8572 13.4531L42.1764 13.4531Z',
code: `<script setup>
import { createClient } from '@supabase/supabase-js'
import { ref, onMounted } from 'vue'
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
)
const todos = ref([])
onMounted(async () => {
const { data } = await supabase
.from('todos').select('*')
todos.value = data
})
</script>`,
lang: 'vue' as const,
docsUrl: '/docs/guides/getting-started/quickstarts/vuejs',
},
{
name: 'Nuxt',
icon: 'M32.5784 45.4741H50.2199C50.7802 45.4741 51.3307 45.3325 51.8159 45.0634C52.3012 44.7943 52.7041 44.4072 52.9842 43.9409C53.2642 43.4748 53.4115 42.946 53.4113 42.4078C53.4111 41.8696 53.2633 41.3409 52.9828 40.875L41.1352 21.164C40.8552 20.6979 40.4524 20.3109 39.9672 20.0418C39.4821 19.7727 38.9317 19.631 38.3715 19.631C37.8113 19.631 37.261 19.7727 36.7758 20.0418C36.2906 20.3109 35.8878 20.6979 35.6078 21.164L32.5784 26.2073L26.6555 16.3452C26.3753 15.8792 25.9723 15.4922 25.487 15.2232C25.0017 14.9541 24.4513 14.8125 23.8909 14.8125C23.3306 14.8125 22.7802 14.9541 22.2949 15.2232C21.8096 14.9541 21.4066 15.8792 21.1263 16.3452L6.38358 40.875C6.10311 41.3409 5.95532 41.8696 5.95508 42.4078C5.95483 42.946 6.10214 43.4748 6.38219 43.9409C6.66224 44.4072 7.06515 44.7943 7.5504 45.0634C8.03564 45.3325 8.58612 45.4741 9.14645 45.4741H20.2203C24.6079 45.4741 27.8436 43.6229 30.07 40.0113L38.3706 26.2073L47.0599 40.6619H35.4754L32.5784 45.4741Z',
code: `<script setup>
const supabase = useSupabaseClient()
const { data: todos } = await useAsyncData(
'todos',
async () => {
const { data } = await supabase
.from('todos')
.select('*')
return data
}
)
</script>
<template>
<TodoList :items="todos" />
</template>`,
lang: 'vue' as const,
docsUrl: '/docs/guides/getting-started/quickstarts/nuxtjs',
},
]
// ── Example links per framework ──────────────────────────────────────────────
const frameworkExamples: Record<
string,
{ title: string; description: string; url: string; icon: string }[]
> = {
React: [
{
title: 'Todo List',
description: 'CRUD app with real-time and RLS.',
url: '/docs/guides/getting-started/tutorials/with-react',
icon: 'ListChecks',
},
{
title: 'Auth & User Management',
description: 'Email, social login, and user profiles.',
url: '/docs/guides/auth/quickstarts/react',
icon: 'Users',
},
],
'Next.js': [
{
title: 'App Router Quickstart',
description: 'Server Components with SSR auth.',
url: '/docs/guides/getting-started/quickstarts/nextjs',
icon: 'Rocket',
},
{
title: 'Auth with PKCE',
description: 'Secure auth flow with middleware.',
url: '/docs/guides/auth/server-side/nextjs',
icon: 'KeyRound',
},
],
Flutter: [
{
title: 'User Management',
description: 'Auth and profile management.',
url: '/docs/guides/getting-started/tutorials/with-flutter',
icon: 'Users',
},
{
title: 'Deep Linking',
description: 'OAuth and magic link for mobile.',
url: '/docs/guides/auth/native-mobile-deep-linking',
icon: 'Link2',
},
],
Svelte: [
{
title: 'Todo List',
description: 'Server-side auth and real-time data.',
url: '/docs/guides/getting-started/tutorials/with-svelte',
icon: 'ListChecks',
},
{
title: 'Auth Helpers',
description: 'Sessions and protected routes.',
url: '/docs/guides/auth/server-side/sveltekit',
icon: 'ShieldCheck',
},
],
Vue: [
{
title: 'Todo List',
description: 'CRUD with Vue 3 Composition API.',
url: '/docs/guides/getting-started/tutorials/with-vue-3',
icon: 'ListChecks',
},
{
title: 'Auth with Vue',
description: 'Email and OAuth authentication.',
url: '/docs/guides/getting-started/quickstarts/vuejs',
icon: 'KeyRound',
},
],
Nuxt: [
{
title: 'Nuxt Module',
description: 'Auto-configured auth and client.',
url: '/docs/guides/getting-started/quickstarts/nuxtjs',
icon: 'Blocks',
},
{
title: 'Server Routes',
description: 'Service role access from server.',
url: '/docs/guides/auth/server-side/nuxt',
icon: 'Server',
},
],
}
// ── Server component: pre-highlights all code ───────────────────────────────
export async function FrameworksSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: ['javascript', 'dart', 'svelte', 'vue'],
})
const frameworks = frameworksList.map((fw) => ({
name: fw.name,
icon: fw.icon,
docsUrl: fw.docsUrl,
darkHtml: hl.codeToHtml(fw.code, { lang: fw.lang, theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(fw.code, { lang: fw.lang, theme: 'supabase-light' }),
examples: frameworkExamples[fw.name] ?? [],
}))
hl.dispose()
return <FrameworksSectionClient frameworks={frameworks} />
}
@@ -0,0 +1,128 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { useState } from 'react'
import { cn } from 'ui'
type Framework = {
name: string
icon: string
docsUrl: string
darkHtml: string
lightHtml: string
examples: { title: string; description: string; url: string; icon: string }[]
}
export function FrameworksSectionClient({ frameworks }: { frameworks: Framework[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const active = frameworks[activeIdx]
const handleTabChange = (index: number) => {
setActiveIdx(index)
}
return (
<div className="border-b border-border py-24">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 md:gap-0">
{/* Left: title */}
<div className="flex flex-col justify-between py-4 gap-6 md:gap-10">
<h3 className="text-2xl md:text-4xl text-foreground-lighter">
Use Supabase with{' '}
<span className="block">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={active.name}
initial={{ filter: 'blur(2px)', opacity: 0 }}
animate={{ filter: 'blur(0px)', opacity: 1, transition: { duration: 0.25 } }}
exit={{ filter: 'blur(2px)', opacity: 0, transition: { duration: 0.1 } }}
className="inline-block text-foreground"
>
{active.name}
</motion.span>
</AnimatePresence>
</span>
</h3>
</div>
{/* Right: icon tabs + code */}
<div className="border border-border flex flex-col rounded-md overflow-clip">
{/* 6-col icon row */}
<div className="grid grid-cols-6 border-b border-border">
{frameworks.map((framework, index) => (
<button
key={framework.name}
onClick={() => handleTabChange(index)}
className={cn(
'flex items-center justify-center py-4 border-r border-border last:border-r-0 transition-colors',
index === activeIdx
? 'bg-surface-75 text-foreground'
: 'text-foreground-muted hover:text-foreground-light hover:bg-surface-75/50'
)}
>
<svg
width={28}
height={28}
fillRule="evenodd"
clipRule="evenodd"
viewBox="0 0 61 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d={framework.icon} fill="currentColor" />
</svg>
</button>
))}
</div>
{/* Code area */}
<div className="relative h-[440px] overflow-auto">
<AnimatePresence mode="wait">
<motion.div
key={active.name}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
>
<div
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
className="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
<div
dangerouslySetInnerHTML={{ __html: active.lightHtml }}
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
<Link
href={active.docsUrl}
className="absolute bottom-4 right-4 flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap"
>
Read docs for <span>{active.name}</span>
<svg
width={12}
height={12}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0"
>
<path
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</div>
</div>
</div>
</div>
</div>
)
}
+54
View File
@@ -0,0 +1,54 @@
'use client'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import Link from 'next/link'
import { Button } from 'ui'
export function Hero() {
const sendTelemetryEvent = useSendTelemetryEvent()
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-32 pb-24 md:pt-40 md:pb-32">
<div className="flex flex-col gap-6 lg:gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-end">
<h1 className="text-foreground text-4xl sm:text-5xl sm:leading-none">
<span className="block">Build in a weekend</span>
<span className="text-brand block">Scale to millions</span>
</h1>
<p className="text-foreground-lighter text-sm text-balance">
Start your project with a Postgres database. Add Authentication, Data APIs, Edge
Functions, Realtime Data, Storage, and Vector embeddings.
</p>
</div>
<div className="flex items-center gap-2">
<Button asChild size="medium">
<Link
href="https://supabase.com/dashboard"
onClick={() =>
sendTelemetryEvent({
action: 'start_project_button_clicked',
properties: { buttonLocation: 'Homepage Hero' },
})
}
>
Start your project
</Link>
</Button>
<Button asChild size="medium" type="default">
<Link
href="/contact/sales"
onClick={() =>
sendTelemetryEvent({
action: 'request_demo_button_clicked',
properties: { buttonLocation: 'Homepage Hero' },
})
}
>
Request a demo
</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,58 @@
'use client'
import getContent from '~/data/home/content'
import dynamic from 'next/dynamic'
import { BuiltWithSupabaseSection } from './BuiltWithSupabaseSection'
import { CommunitySectionV2 as CommunitySection } from './CommunitySectionV2'
import { CTASection } from './CTASection'
import { CustomerStoriesSection } from './CustomerStoriesSection'
import { DashboardFeaturesSection } from './DashboardFeaturesSection'
import { Hero } from './Hero'
import { LogosGrid } from './LogosGrid'
import { OpenSourceSection } from './OpenSourceSection'
const Products = dynamic(() => import('~/components/Products/index'))
function SectionBlock({ children, className }: { children: React.ReactNode; className?: string }) {
return <section className={`border-b border-border ${className ?? ''}`}>{children}</section>
}
export function HomeContent({ frameworksSlot }: { frameworksSlot: React.ReactNode }) {
const content = getContent()
return (
<div className="[&_.container]:!max-w-[var(--container-max-w,75rem)] [&_.container]:!px-6">
<section>
<Hero />
</section>
<SectionBlock className="border-b-0 relative">
<div className="-translate-y-16">
<Products {...content.productsSection} />
</div>
</SectionBlock>
<section>
<LogosGrid />
</section>
<section>
<DashboardFeaturesSection {...content.dashboardFeatures} />
</section>
<section>{frameworksSlot}</section>
<section className="border-b border-border">
<BuiltWithSupabaseSection />
</section>
<section>
<CustomerStoriesSection />
</section>
<section>
<CommunitySection />
</section>
<section>
<OpenSourceSection />
</section>
<section>
<CTASection />
</section>
</div>
)
}
@@ -0,0 +1,113 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { ComponentProps, useEffect, useState } from 'react'
import {
BetasharesLogo,
BoltLogo,
ChatbaseLogo,
FigmaLogo,
GithubLogo,
GoodtapeLogo,
GopuffLogo,
GumloopLogo,
HappyteamsLogo,
HumataLogo,
LangchainLogo,
LoopsLogo,
LovableLogo,
MarkpromptLogo,
MdnLogo,
MobbinLogo,
MozillaLogo,
PikaLogo,
PwcLogo,
ResendLogo,
SoshiLogo,
SubmagicLogo,
TempoLogo,
V0Logo,
} from './logos/PublicityLogos'
const gridLogos: { name: string; Logo: (props: ComponentProps<'svg'>) => React.JSX.Element }[] = [
{ name: 'betashares', Logo: BetasharesLogo },
{ name: 'bolt', Logo: BoltLogo },
{ name: 'chatbase', Logo: ChatbaseLogo },
{ name: 'figma', Logo: FigmaLogo },
{ name: 'github', Logo: GithubLogo },
{ name: 'goodtape', Logo: GoodtapeLogo },
{ name: 'gopuff', Logo: GopuffLogo },
{ name: 'gumloop', Logo: GumloopLogo },
{ name: 'happyteams', Logo: HappyteamsLogo },
{ name: 'humata', Logo: HumataLogo },
{ name: 'langchain', Logo: LangchainLogo },
{ name: 'loops', Logo: LoopsLogo },
{ name: 'lovable', Logo: LovableLogo },
{ name: 'markprompt', Logo: MarkpromptLogo },
{ name: 'mdn', Logo: MdnLogo },
{ name: 'mobbin', Logo: MobbinLogo },
{ name: 'mozilla', Logo: MozillaLogo },
{ name: 'pika', Logo: PikaLogo },
{ name: 'pwc', Logo: PwcLogo },
{ name: 'resend', Logo: ResendLogo },
{ name: 'soshi', Logo: SoshiLogo },
{ name: 'submagic', Logo: SubmagicLogo },
{ name: 'tempo', Logo: TempoLogo },
{ name: 'v0', Logo: V0Logo },
]
const LOGOS_PER_PAGE = 12
export function LogosGrid() {
const [page, setPage] = useState(0)
const totalPages = Math.ceil(gridLogos.length / LOGOS_PER_PAGE)
useEffect(() => {
const interval = setInterval(() => {
setPage((p) => (p + 1) % totalPages)
}, 10000)
return () => clearInterval(interval)
}, [totalPages])
const start = page * LOGOS_PER_PAGE
const currentLogos = gridLogos.slice(start, start + LOGOS_PER_PAGE)
return (
<div>
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 py-6">
<p className="text-sm text-foreground-lighter">
Trusted by fast-growing companies worldwide
</p>
</div>
<div className="border-y border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border py-10">
<AnimatePresence mode="popLayout" initial={false}>
<motion.div
key={page}
className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-y-10 gap-x-6"
>
{currentLogos.map(({ name, Logo }, i) => (
<motion.div
key={name}
initial={{ opacity: 0, scale: 0.95, filter: 'blur(2px)' }}
animate={{ opacity: 0.7, scale: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, scale: 0.95, filter: 'blur(2px)' }}
transition={{
duration: 0.45,
delay: i * 0.03,
ease: [0.25, 0.46, 0.45, 0.94],
}}
className="flex items-center justify-center h-10 text-foreground-lighter"
>
<Logo className="h-8 lg:h-12 w-auto" />
</motion.div>
))}
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
)
}
@@ -0,0 +1,197 @@
'use client'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { useInView } from 'framer-motion'
import Link from 'next/link'
import { useRef } from 'react'
import { Button } from 'ui'
// ── Pixel font (5×7 per glyph) ─────────────────────────────────────────────
const GLYPHS: Record<string, string[]> = {
'0': ['01110', '10001', '10011', '10101', '11001', '10001', '01110'],
'1': ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
'2': ['01110', '10001', '00001', '00110', '01000', '10000', '11111'],
'3': ['01110', '10001', '00001', '00110', '00001', '10001', '01110'],
'4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'],
'5': ['11111', '10000', '11110', '00001', '00001', '10001', '01110'],
'6': ['01110', '10001', '10000', '11110', '10001', '10001', '01110'],
'7': ['11111', '00001', '00010', '00100', '01000', '01000', '01000'],
'8': ['01110', '10001', '10001', '01110', '10001', '10001', '01110'],
'9': ['01110', '10001', '10001', '01111', '00001', '10001', '01110'],
'.': ['00000', '00000', '00000', '00000', '00000', '00000', '00100'],
K: ['10001', '10010', '10100', '11000', '10100', '10010', '10001'],
' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'],
}
function textToPixelMask(text: string): Set<string> {
const mask = new Set<string>()
let cursorX = 0
for (const ch of text) {
const glyph = GLYPHS[ch]
if (!glyph) {
cursorX += 3
continue
}
for (let row = 0; row < glyph.length; row++) {
for (let col = 0; col < glyph[row].length; col++) {
if (glyph[row][col] === '1') mask.add(`${cursorX + col},${row}`)
}
}
cursorX += 6 // 5 wide + 1 gap
}
return mask
}
// ── Contribution graph ──────────────────────────────────────────────────────
const CELL = 3
const GAP = 0.5
const COLS = 72
const ROWS = 40
const TEXT = '98.4K'
const TEXT_MASK = textToPixelMask(TEXT)
const TEXT_W = TEXT.length * 6 - 1
const TEXT_H = 7
// Position text toward the right
const TEXT_OFFSET_X = Math.floor(COLS * 0.56 - TEXT_W / 2)
const TEXT_OFFSET_Y = Math.floor((ROWS - TEXT_H) / 2)
const LEVELS_BG = [
'hsl(var(--background-surface-300) / 0.2)',
'hsl(var(--brand-400) / 0.08)',
'hsl(var(--brand-500) / 0.1)',
'hsl(var(--brand-600) / 0.12)',
'hsl(var(--brand-default) / 0.12)',
]
const LEVELS_TEXT = [
'hsl(var(--brand-600))',
'hsl(var(--brand-default))',
'hsl(var(--brand-default))',
'hsl(var(--brand-600))',
'hsl(var(--brand-500))',
]
function makeGrid(): { level: number; isText: boolean }[][] {
let seed = 0x9e3779b9
const rand = () => {
seed ^= seed << 13
seed ^= seed >> 17
seed ^= seed << 5
return (seed >>> 0) / 0xffffffff
}
return Array.from({ length: COLS }, (_, c) =>
Array.from({ length: ROWS }, (_, r) => {
const v = rand()
const level = v < 0.45 ? 0 : v < 0.65 ? 1 : v < 0.8 ? 2 : v < 0.92 ? 3 : 4
const localX = c - TEXT_OFFSET_X
const localY = r - TEXT_OFFSET_Y
const isText = TEXT_MASK.has(`${localX},${localY}`)
return { level, isText }
})
)
}
const GRID = makeGrid()
const SVG_W = COLS * (CELL + GAP) - GAP
const SVG_H = ROWS * (CELL + GAP) - GAP
function ContribGraph() {
const ref = useRef<SVGSVGElement>(null)
const isInView = useInView(ref, { once: true, margin: '-50px' })
return (
<svg
ref={ref}
viewBox={`0 0 ${SVG_W} ${SVG_H}`}
preserveAspectRatio="xMaxYMid slice"
className="absolute inset-0 h-full w-full"
aria-hidden
>
{GRID.map((col, c) =>
col.map(({ level, isText }, r) => (
<rect
key={`${c}-${r}`}
x={c * (CELL + GAP)}
y={r * (CELL + GAP)}
width={CELL}
height={CELL}
rx={1}
fill={isText ? LEVELS_TEXT[level] : LEVELS_BG[level]}
opacity={isInView ? 1 : 0}
style={{
transition: `opacity ${isText ? '0.6s' : '0.4s'} ease ${isText ? 0.3 + c * 0.015 : c * 0.02 + r * 0.01}s`,
}}
/>
))
)}
</svg>
)
}
// ── Component ───────────────────────────────────────────────────────────────
export function OpenSourceSection() {
const sendTelemetryEvent = useSendTelemetryEvent()
const starsNum = 98400
return (
<div className="border-b border-border overflow-hidden">
<div className="relative py-24 flex flex-col justify-center">
{/* Contrib graph background — right-aligned, radial fade */}
<div
className="hidden md:block absolute inset-y-0 left-1/4 right-[calc((100%-var(--container-max-w,75rem))/2*-1)]"
style={{
maskImage: 'radial-gradient(ellipse 55% 90% at 58% 50%, black 15%, transparent 60%)',
WebkitMaskImage:
'radial-gradient(ellipse 55% 90% at 58% 50%, black 15%, transparent 60%)',
}}
>
<ContribGraph />
</div>
<div className="relative z-10 w-full max-w-[var(--container-max-w,75rem)] mx-auto px-6">
<div className="flex flex-col items-center text-center md:items-start md:text-left justify-between gap-6 py-10 max-w-lg md:max-w-lg mx-auto md:mx-0">
<div>
<h2 className="text-4xl text-foreground text-balance">Open source from day one</h2>
<p className="mt-4 text-sm leading-relaxed text-foreground-lighter">
Supabase is built in the open because we believe great developer tools should be
transparent, inspectable, and owned by the community. Read, contribute, self-host.
You&apos;re never locked in, and always in control.
</p>
</div>
<div className="flex items-center gap-3">
<Button
asChild
size="small"
type="default"
icon={
<svg viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.5 2.22168C5.23312 2.22168 2.58496 4.87398 2.58496 8.14677C2.58496 10.7642 4.27962 12.9853 6.63026 13.7684C6.92601 13.8228 7.03366 13.6401 7.03366 13.4827C7.03366 13.3425 7.02893 12.9693 7.02597 12.4754C5.38041 12.8333 5.0332 11.681 5.0332 11.681C4.76465 10.996 4.37663 10.8139 4.37663 10.8139C3.83954 10.4471 4.41744 10.4542 4.41744 10.4542C5.01072 10.4956 5.32303 11.0647 5.32303 11.0647C5.85065 11.9697 6.70774 11.7082 7.04431 11.5568C7.09873 11.1741 7.25134 10.9132 7.42051 10.7654C6.10737 10.6157 4.72621 10.107 4.72621 7.83683C4.72621 7.19031 4.95689 6.66092 5.33486 6.24686C5.27394 6.09721 5.07105 5.49447 5.39283 4.67938C5.39283 4.67938 5.88969 4.51967 7.01947 5.28626C7.502 5.15466 7.99985 5.08763 8.5 5.08692C9.00278 5.08929 9.50851 5.15495 9.98113 5.28626C11.1103 4.51967 11.606 4.67879 11.606 4.67879C11.9289 5.49447 11.7255 6.09721 11.6651 6.24686C12.0437 6.66092 12.2732 7.19031 12.2732 7.83683C12.2732 10.1129 10.8897 10.6139 9.5724 10.7606C9.78475 10.9434 9.97344 11.3048 9.97344 11.8579C9.97344 12.6493 9.96634 13.2887 9.96634 13.4827C9.96634 13.6413 10.0728 13.8258 10.3733 13.7678C11.5512 13.3728 12.5751 12.6175 13.3003 11.6089C14.0256 10.6002 14.4155 9.38912 14.415 8.14677C14.415 4.87398 11.7663 2.22168 8.5 2.22168Z"
fill="currentColor"
/>
</svg>
}
>
<Link
href="https://github.com/supabase"
target="_blank"
onClick={() => sendTelemetryEvent({ action: 'homepage_github_button_clicked' })}
>
View on GitHub
</Link>
</Button>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,277 @@
import { ComponentProps } from 'react'
export function DartLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 62 62" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="m40.912 15.32-5.008-5.007c-.652-.655-2.013-1.438-3.163-1.438-.989 0-1.958.2-2.588.575l-11.54 5.77h21.23q.543 0 1.069.1m-25.269 2.507-6.191 12.9c-.248.523-.575 1.431-.575 2.012 0 1.256.552 2.54 1.438 3.452l5.36 5.32a6 6 0 0 1-.032-.597zm5.075 28.763 5.704 5.705.29.289h18.116V46.66H21.643q-.467 0-.925-.07m25.324-1.77h6.55V27.4l-9.617-9.617a4.43 4.43 0 0 0-3.132-1.298H17.53l.27.27 27.918 27.742zm-27.874-.865a4.92 4.92 0 0 0 3.475 1.44h23.183l-1.273-1.266-25.379-25.22-1.265-1.257v23.262a4.3 4.3 0 0 0 1.26 3.041"
clipRule="evenodd"
/>
</svg>
)
}
export function ExpoLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 61 61" {...props}>
<path
fill="currentColor"
d="M45.5 25.727a3.35 3.35 0 0 0-1.31-1.08 3.42 3.42 0 0 0-3.303.233 3.3 3.3 0 0 0-1.137 1.254 2.915 2.915 0 0 0 .579 3.32 4.97 4.97 0 0 0 3.292-.92 4.8 4.8 0 0 0 1.878-2.807m-9.995-12.813-2.14-1.151-9.317 5.083.743.394 1.413.727 2.557-1.393 6.746-3.684zm.863-.351a.33.33 0 0 1 .236.22l3.121 9.026a.296.296 0 0 1-.148.393 5.96 5.96 0 0 0-2.654 2.882 5.8 5.8 0 0 0-.259 3.877 6.26 6.26 0 0 0 2.591 3.405 6.48 6.48 0 0 0 4.214.993.4.4 0 0 1 .387.234l3.224 9.265a.37.37 0 0 1-.149.406l-9.91 5.68a.4.4 0 0 1-.148.029.4.4 0 0 1-.268-.044l-3.477-2.14a.34.34 0 0 1-.147-.145l-6.792-15.307-10.342 5.768a.49.49 0 0 1-.402.014l-2.35-1.311a.333.333 0 0 1-.148-.437l10.047-18.69a.36.36 0 0 1 .163-.145l10.017-5.477a.39.39 0 0 1 .356 0l2.84 1.504zm-12.423 5.022-.487-.247-9.685 18.077 1.767.975 8.352-10.71a.4.4 0 0 1 .341-.146.42.42 0 0 1 .297.218l9.094 20.539 2.43 1.47-9.761-27.977-.415-1.165-1.946-1.049.015.015zm14.978 10.779a3.63 3.63 0 0 1 .177-2.428c.339-.77.931-1.403 1.68-1.797a4.17 4.17 0 0 1 4.815.585 3.635 3.635 0 0 1 .108 5.21l-.108.106a4.11 4.11 0 0 1-2.793 1.1 4.12 4.12 0 0 1-2.8-1.085 3.97 3.97 0 0 1-1.079-1.691"
/>
</svg>
)
}
export function FlutterLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 62 61" {...props}>
<path
fill="currentColor"
d="m46.507 10.383-12.156.013L14.75 29.997l6.047 6.055 5.316-5.285zM34.7 28.465c-.173-.008-.35-.016-.462.131L23.786 39.048l6.003 5.967-.007.007 4.297 4.3q.042.038.085.082c.117.117.243.243.425.203q2.977-.006 5.955-.004 2.977.003 5.956-.002L35.933 39.023l10.563-10.556-11.59-.003c-.064.008-.135.005-.206.001"
/>
</svg>
)
}
export function FlutterflowLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 61 62" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M44.714 12.64a2.91 2.91 0 0 1 2.571 1.515 2.87 2.87 0 0 1-.053 2.836l-.018.03-4.517 7.503a2.88 2.88 0 0 1-2.42 1.39h-5.191l2.145 4.874.008.014.013.023a2.87 2.87 0 0 1-.03 2.877l-.018.03-4.518 7.504a2.88 2.88 0 0 1-2.419 1.389h-6.364l-4.552 4.767-.009.009a2.27 2.27 0 0 1-2.061.628 2.27 2.27 0 0 1-1.642-1.342l-.009-.02-2.395-5.46-.013-.02-.013-.023-.006-.012a2.87 2.87 0 0 1 .03-2.877l.018-.03 4.382-7.278-3.229-7.339.06-.03-.002-.004a2.87 2.87 0 0 1 .325-1.996l.019-.034.018-.03 4.518-7.504a2.88 2.88 0 0 1 2.419-1.39h22.953M20.992 42.569h-4.8l1.41 3.238.003.009q.035.086.118.101c.05.01.09 0 .13-.035l.006-.007zm13.723-11.133h-14.51l-.048.002h-.005l3.979 9.036h6.103c.251 0 .493-.133.63-.347l.011-.018 4.524-7.509a.76.76 0 0 0 .043-.717.79.79 0 0 0-.727-.447m-16.019 1.856-3.59 6.009a.77.77 0 0 0-.024.766l.014.025.007.012.015.024.028.042.032.042.031.034.005.006.021.02.018.018a.7.7 0 0 0 .407.18l.034.003h.02l.026.001h6.092zm-.944-7.363 1.577 3.592.031-.01c.272-.091.557-.14.845-.145h14.108l-1.51-3.437zm26.922-11.192H30.113l3.98 9.038h6.1a.76.76 0 0 0 .631-.348l.011-.018L45.36 15.9a.76.76 0 0 0 .043-.717.79.79 0 0 0-.709-.447zm-16.79 0h-6.068c-.25 0-.49.133-.627.347l-.01.017-4.514 7.509a.76.76 0 0 0-.042.718.79.79 0 0 0 .704.446h14.526z"
clipRule="evenodd"
/>
</svg>
)
}
export function KotlinLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" {...props}>
<path
fill="currentColor"
strokeMiterlimit="10"
d="M29.6 11.4 11.4 28.2V13.4c0-1.1 1-2 2.1-2zm15.5 3.3L25.4 32.9 11.6 45.5q-.3-.45-.3-.9V33.7l24.2-22.3h8c2 0 2.9 2.1 1.6 3.3m-.6 31.9h-28L33 31.4l13 11.9c1.3 1.2.4 3.3-1.5 3.3"
transform="scale(10.66667)"
/>
</svg>
)
}
export function LangchainLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 100 100" {...props}>
<rect width="100" height="100" rx="20" fill="currentColor" />
<path
d="M54.2612 54.2583L63.1942 45.3253C67.8979 40.6215 67.8979 32.9952 63.1942 28.2914C58.4904 23.5877 50.8641 23.5877 46.1603 28.2914L37.2273 37.2244"
stroke="#7FC8FF"
strokeWidth="12.0389"
/>
<path
d="M45.7427 45.7412L36.8098 54.6742C32.106 59.3779 32.106 67.0042 36.8098 71.708C41.5135 76.4118 49.1398 76.4118 53.8436 71.708L62.7766 62.775"
stroke="#7FC8FF"
strokeWidth="12.0389"
/>
</svg>
)
}
export function NestjsLogo(props: ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 264.6 255.6"
{...props}
>
<path
d="M153.3 4.2c-1.8 0-3.5.4-5 1 3.3 2.1 5.1 5 6 8.3 0 .5.2.8.3 1.3l.1 1.1c.3 5.7-1.5 6.4-2.7 9.8-1.9 4.3-1.4 9 .9 12.7.2.5.4 1 .8 1.4-2.5-16.3 11.1-18.8 13.7-23.9.2-4.4-3.5-7.4-6.4-9.5a14.3 14.3 0 0 0-7.7-2.2zM174 8c-.3 1.5 0 1.1-.1 1.9l-.2 1.7-.4 1.5-.5 1.6-.8 1.5-.5.7-.4.6c-.3.5-.6 1-1 1.3-.3.4-.6.9-1 1.2l-1.3 1c-1.4 1.1-3 1.9-4.3 2.9-.5.3-1 .5-1.3 1-.5.2-.9.6-1.3 1l-1.1 1.2-1 1.3-.9 1.3-.7 1.5-.5 1.5a21 21 0 0 0-.5 1.6l-.1.9-.1.7-.1 1.7v1.1l.3 1.6c0 .6.1 1 .3 1.6l.5 1.5.4 1-14.8-5.8-7.5-2-4-1a120 120 0 0 0-11.8-1.7h-.4A115.5 115.5 0 0 0 87 34.9l-3 .6c-2 .3-3.9.8-5.7 1.2l-3 .8-2.7 1.2-2.2 1-.3.1-1.8 1-.5.1-2 1-1.2.7-.6.3-1.7 1-1.6 1-1.3.9-.1.1-1.3 1H58l-1 .8-.4.3-1 .8c0 .2-.1.2-.2.3l-1.2 1v.2c-.5.3-.9.7-1.2 1.1l-.2.1-1 1c0 .2-.3.3-.4.5l-1 1.1-.4.3-1.4 1.6-.2.2a38.1 38.1 0 0 1-7 6 48.9 48.9 0 0 1-12.1 6c-2.7.5-5.5 1.6-7.9 1.8l-1.6.2-1.6.4-1.6.6-1.5.7-1.4.9c-.5.3-1 .7-1.3 1.1-.5.3-1 .8-1.3 1.2l-1.1 1.3-1 1.4-.9 1.5-.7 1.7-.6 1.7-.3 1.5v.2L6 86.2v2.1a6.9 6.9 0 0 0 .7 2.4l.7 1.2.8 1.2a17.1 17.1 0 0 0 2.4 2c1.5 1.4 1.9 1.9 3.9 2.9l1 .5h.2v.4a13.3 13.3 0 0 0 1 3.1l.5 1.2.1.3a28.3 28.3 0 0 0 1.8 2.8l1 1.2 1.3 1.1h.1a14.2 14.2 0 0 0 5.4 3l.3.1.8.2c-.2 3.5-.3 6.8.3 8 .5 1.2 3.4-2.7 6.2-7.2-.4 4.4-.6 9.7 0 11.2.7 1.6 4.6-3.4 8-9a74.7 74.7 0 0 1 92 65.8c-.8-7-9.4-10.8-13.4-9.9-2 4.8-5.2 11-10.5 14.8.4-4.3.2-8.7-.7-13-1.4 6-4.2 11.5-8 16.3a18 18 0 0 1-15.5-7l-.5-.8-.5-1.4-.4-1.3V176c0-.5.1-1 .3-1.4 0-.4.2-.9.4-1.3l.8-1.4c1-3 1-5.6-1-7l-1.1-.7-.9-.3-.5-.2-1.4-.3a5 5 0 0 0-1.3-.2l-1.4-.1h-1l-1.4.2-1.4.3-1.3.4-1.3.6-1.3.7c-15 9.8-6 32.8 4.2 39.5-3.8.7-7.8 1.5-8.9 2.3l-.1.2a60.9 60.9 0 0 0 19.2 7.4 61.5 61.5 0 0 0 72.6-51.3l.4 1.7c.2 1.2.5 2.4.6 3.7l.2 1.7v.3l.2 1.6.1 2.2v5.4l-.1.8v1.5c-.2.2-.2.4-.2.5 0 .6 0 1-.2 1.5v.6c0 .7-.2 1.2-.3 1.9v.1l-.4 1.8v.2c0 .6-.2 1.2-.4 1.8v.2l-.5 1.8v.2l-.5 1.8v.1l-.6 2-.7 1.8-.8 1.9-.7 1.9c-.4.5-.6 1.2-1 1.8l-.1.4s0 .2-.2.2a61.2 61.2 0 0 1-18.1 21.7l-1.6 1.1c0 .2-.3.2-.4.4l-1.4 1 .2.3 2.7-.4h.1a137.7 137.7 0 0 0 6.5-1.2l.9-.2 1.3-.3 1.2-.3c6.4-1.5 12.7-3.7 18.7-6.2-10.2 14-24 25.3-40.1 32.8a103.2 103.2 0 0 0 83.1-52.6c-2.7 15-8.6 29.1-17.4 41.5a101.7 101.7 0 0 0 44.5-69.2c2.2 10.2 2.8 20.7 1.8 31.1 46.7-65 4-132.5-14-150.3l-.1-.3v.1l-.1-.1-.2 2.3a87 87 0 0 1-.6 4.3l-1.1 4.3a53.7 53.7 0 0 1-3.5 8 44 44 0 0 1-9.9 12l-1.5 1.4a36 36 0 0 1-7.4 4.7l-4 1.8a45.5 45.5 0 0 1-8.6 2.3l-4.4.6a49.7 49.7 0 0 1-11.9-.8l-4.3-1.1a48 48 0 0 0 20.7-6.8l3.6-2.6 3.3-2.9 3-3.2c1-1.1 1.9-2.3 2.7-3.5.2-.1.3-.4.4-.6l1.9-3.1a44.5 44.5 0 0 0 3.5-8c.4-1.4.8-2.9 1-4.3.3-1.5.6-2.9.7-4.3l.3-4.4-.1-3.1-.6-4.3c-.2-1.5-.5-3-1-4.4-.4-1.3-.8-2.7-1.4-4.1-.5-1.4-1.1-2.7-1.8-4l-2.2-3.8a71.3 71.3 0 0 0-5.5-6.9 40.4 40.4 0 0 0-12-8.6C178 9.3 176 8.6 174 8z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
)
}
export function NextjsLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 180 180" {...props}>
<mask
height="180"
id=":r8:mask0_408_134"
maskUnits="userSpaceOnUse"
width="180"
x="0"
y="0"
style={{ maskType: 'alpha' as const }}
>
<circle cx="90" cy="90" fill="currentColor" r="90"></circle>
</mask>
<g mask="url(#:r8:mask0_408_134)">
<circle cx="90" cy="90" data-circle="true" fill="currentColor" r="90"></circle>
<path
d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z"
fill="currentColor"
></path>
<rect fill="currentColor" height="72" width="12" x="115" y="54"></rect>
</g>
</svg>
)
}
export function NuxtjsLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 61 61" {...props}>
<path
fill="currentColor"
d="M32.578 45.474H50.22c.56 0 1.11-.141 1.596-.41a3.13 3.13 0 0 0 1.168-1.123c.28-.466.427-.995.427-1.533s-.148-1.067-.428-1.533L41.135 21.164a3.13 3.13 0 0 0-1.168-1.122 3.3 3.3 0 0 0-1.596-.411c-.56 0-1.11.142-1.595.41a3.13 3.13 0 0 0-1.168 1.123l-3.03 5.043-5.923-9.862a3.13 3.13 0 0 0-1.168-1.122 3.3 3.3 0 0 0-1.596-.41c-.56 0-1.11.141-1.596.41a3.13 3.13 0 0 0-1.169 1.122L6.384 40.875c-.28.466-.429.995-.429 1.533s.147 1.067.427 1.533.683.853 1.168 1.122c.486.27 1.036.411 1.596.411H20.22c4.388 0 7.624-1.851 9.85-5.463l8.3-13.804 8.69 14.455H35.475zM20.04 40.657l-7.728-.002 11.584-19.271 5.78 9.636-3.87 6.44c-1.478 2.343-3.158 3.197-5.766 3.197"
/>
</svg>
)
}
export function OpenaiLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 260" {...props}>
<path d="M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z" />
</svg>
)
}
export function ReactLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 62 62" {...props}>
<path
fill="currentColor"
d="M45.74 23.698a30 30 0 0 0-1.446-.455q.122-.501.223-.987c1.095-5.314.38-9.595-2.065-11.005-2.344-1.352-6.177.058-10.048 3.427a30 30 0 0 0-1.12 1.026 28 28 0 0 0-.744-.687c-4.057-3.603-8.124-5.121-10.566-3.707-2.341 1.355-3.035 5.38-2.05 10.417q.144.73.333 1.488-.864.244-1.663.523C11.838 25.396 8.8 27.995 8.8 30.69c0 2.784 3.26 5.577 8.215 7.27q.587.2 1.215.38a29 29 0 0 0-.353 1.604c-.94 4.95-.206 8.88 2.13 10.226 2.412 1.391 6.46-.039 10.402-3.484q.468-.41.938-.864.609.586 1.215 1.108c3.818 3.285 7.589 4.612 9.922 3.262 2.41-1.395 3.193-5.617 2.176-10.753a25 25 0 0 0-.27-1.201q.427-.125.837-.261c5.15-1.706 8.501-4.465 8.501-7.287 0-2.706-3.136-5.322-7.988-6.993m-1.117 12.458q-.37.122-.755.236c-.568-1.8-1.336-3.714-2.275-5.692.896-1.93 1.634-3.82 2.187-5.608q.69.2 1.335.421c4.158 1.431 6.694 3.547 6.694 5.178 0 1.737-2.739 3.992-7.186 5.465m-1.845 3.657c.45 2.27.513 4.324.216 5.93-.268 1.442-.806 2.403-1.471 2.788-1.416.82-4.444-.245-7.71-3.055a28 28 0 0 1-1.13-1.03c1.267-1.385 2.532-2.995 3.767-4.782 2.171-.193 4.223-.508 6.084-.938q.138.555.244 1.087M24.117 48.39c-1.383.488-2.485.502-3.151.118-1.417-.817-2.007-3.972-1.203-8.205q.139-.727.328-1.491c1.84.407 3.877.7 6.055.876a47 47 0 0 0 3.856 4.765q-.43.416-.856.788c-1.743 1.524-3.49 2.605-5.03 3.149m-6.48-12.245c-2.192-.748-4-1.722-5.241-2.783-1.114-.955-1.677-1.902-1.677-2.67 0-1.637 2.44-3.724 6.507-5.142q.741-.258 1.547-.486c.562 1.828 1.3 3.74 2.19 5.674a47 47 0 0 0-2.218 5.755q-.574-.165-1.109-.348m2.171-14.787c-.844-4.315-.283-7.57 1.128-8.387 1.503-.87 4.827.37 8.33 3.48q.336.3.674.623a46 46 0 0 0-3.827 4.736c-2.113.196-4.136.51-5.996.932q-.176-.706-.309-1.384m19.38 4.785a61 61 0 0 0-1.366-2.247 42 42 0 0 1 4.097.717 41 41 0 0 1-1.438 3.868 67 67 0 0 0-1.292-2.338m-7.903-7.697a41 41 0 0 1 2.644 3.191 57 57 0 0 0-5.31 0 42 42 0 0 1 2.666-3.191m-7.953 7.71q-.666 1.157-1.269 2.324a42 42 0 0 1-1.427-3.888 43 43 0 0 1 4.073-.699 56 56 0 0 0-1.377 2.263m1.418 11.47a41 41 0 0 1-4.14-.666 43 43 0 0 1 1.456-3.966 56.791 56.791 0 0 0 2.685 4.632m6.589 5.446a43 43 0 0 1-2.704-3.235q1.291.05 2.629.05 1.375 0 2.712-.06a41 41 0 0 1-2.637 3.245m9.16-10.147a41 41 0 0 1 1.51 3.952c-1.304.297-2.71.537-4.192.715q.7-1.11 1.38-2.286.686-1.192 1.302-2.381m-2.965 1.422a63 63 0 0 1-2.156 3.477c-1.333.095-2.71.145-4.114.145a63 63 0 0 1-4.068-.129A54 54 0 0 1 25 34.357a54 54 0 0 1-1.917-3.62 54 54 0 0 1 1.912-3.622 54 54 0 0 1 2.178-3.468 54 54 0 0 1 4.09-.153h.001c1.391 0 2.763.053 4.099.155a58 58 0 0 1 2.164 3.456 63 63 0 0 1 1.937 3.605 58 58 0 0 1-1.93 3.637m3.96-21.433c1.504.867 2.089 4.366 1.143 8.955q-.09.439-.202.893a46 46 0 0 0-6.007-.949c-1.234-1.758-2.514-3.356-3.798-4.74q.518-.499 1.034-.948c3.316-2.886 6.417-4.026 7.83-3.211m-10.23 13.765a4.012 4.012 0 1 1 0 8.024 4.012 4.012 0 0 1 0-8.024"
/>
</svg>
)
}
export function ReactiveXLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 62 62" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="m29.323 12.449.005-.012s-.53-.514-.868-.74c-1.831-1.22-4.562-1.622-6.217.097-.287.297-.548.61-.806.92-.19.227-.379.453-.576.67-.53.58-1.172 1.061-1.911 1.334-.415.156-.83.19-1.248.226-.23.02-.462.04-.696.08-.675.112-1.366.353-1.912.787-.594.482-.835 1.124-.9 1.863-.04.361-.049.729-.057 1.095-.006.22-.01.44-.023.657-.08 1.703-.627 2.185-1.848 3.132-.514.386-.947.9-1.269 1.446-.609 1.077-.217 2.18.175 3.279.227.64.454 1.278.484 1.91.016.354-.016.707-.145 1.045-.09.258-.242.454-.397.653a5 5 0 0 0-.197.263c-.29.401-.482.883-.402 1.381s.338.964.579 1.398c.466.771 1.044 1.462 1.654 2.12.026-.012.052.016.078.044v.002c.572 1.792 1.428 3.518 2.525 5.16 4.177 6.233 10.956 8.386 10.956 8.386 10.04 3.406 16.9-1.606 16.9-1.606 6.314-4.338 7.583-9.35 7.583-9.35-5.092 3.968-7.293 3.582-7.293 3.582 3.695-1.607 5.27-4.386 5.27-4.386 3.453-4.707 2.28-9.671 2.28-9.671-2.2 4.787-4.209 6.105-4.209 6.105 5.414-8.242.032-13.223.032-13.223 2.201 4.692-.722 10.411-.722 10.411-2.458 5.173-5.945 7.02-5.945 7.02 3.888.724 6.748-1.895 6.748-1.895-5.575 6.025-11.615 5.736-11.615 5.736 2.538 2.843 6.345 2.602 6.345 2.602-4.98 1.173-9.655-.482-13.494-3.679a21 21 0 0 1-2.057-1.928s-.578-.61-.69-.771l-.017-.016c-.08 2.972 3.02 5.736 3.02 5.736-3.887-1.607-5.67-5.093-5.67-5.093s-2.619-4.467-.723-9.56c1.895-5.092 7.63-6.185 7.63-6.185 4.74 2.297 8.756 3.02 8.756 3.02 8.467 1.414 7.985-2.73 7.985-2.73.08-3.567-5.302-7.359-5.302-7.359-4.366-3.232-8.542-4.038-11.796-3.956m6.334 6.382c.482 0 .868.386.868.867a.864.864 0 0 1-.868.868.864.864 0 0 1-.867-.867c0-.482.385-.868.867-.868m-5.494-1.542-5.478-1.703q-.01 0-.032-.007a.8.8 0 0 0-.45.007s-3.23.819-2.667 2.586c0 0 .337 1.109 1.253 2.185l6.025-.289z"
clipRule="evenodd"
/>
</svg>
)
}
export function RedwoodLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 62 61" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="m22.67 16.278 7.525 5.119c.17.112.368.174.572.177.203-.001.402-.063.571-.178l7.53-5.135a1.023 1.023 0 0 0-.143-1.75L31.201 10.8a1.04 1.04 0 0 0-.903 0l-7.502 3.712a1.021 1.021 0 0 0-.125 1.767m10.652 6.725c0 .34.168.657.446.85L39.8 27.96c.386.265.903.232 1.252-.08l5.06-4.51a1.023 1.023 0 0 0-.045-1.567l-4.832-3.85a1.03 1.03 0 0 0-1.212-.046l-6.255 4.263c-.273.189-.44.499-.446.832M18.32 28.592c.244.218.37.54.338.866-.032.328-.222.62-.51.78l-3.601 2.152a1.02 1.02 0 0 1-1.159-.079 1.03 1.03 0 0 1-.34-1.114l1.333-4.177a1.02 1.02 0 0 1 .703-.677.99.99 0 0 1 .95.223zm19.537.436-6.512-4.441a1.03 1.03 0 0 0-1.143 0l-6.513 4.44a1.04 1.04 0 0 0-.446.781c-.017.317.11.625.343.838l6.507 5.8a1.02 1.02 0 0 0 1.36 0l6.507-5.8c.235-.213.36-.521.337-.838a1.02 1.02 0 0 0-.44-.78m-17.37-1.148-5.054-4.51a1.05 1.05 0 0 1-.343-.809c.005-.302.146-.587.383-.774l4.831-3.873a1.04 1.04 0 0 1 1.218-.046l6.25 4.263a1.028 1.028 0 0 1 0 1.721l-6.027 4.108a1.026 1.026 0 0 1-1.258-.08m25.952 6.478-5.146-3.07a1.015 1.015 0 0 0-1.2.115l-6.29 5.595a1.03 1.03 0 0 0 .298 1.721l8.725 3.523q.182.075.377.075c.405 0 .773-.238.938-.609l2.704-6.036a1.03 1.03 0 0 0-.406-1.314m.715-7.338 1.332 4.177h-.011a1.03 1.03 0 0 1-.15.917 1.023 1.023 0 0 1-1.348.276l-3.608-2.152a1 1 0 0 1-.492-.78c-.033-.326.092-.65.338-.866L45.5 26.56c.258-.231.615-.315.949-.224.333.095.597.351.703.683M28.068 37.939a1.02 1.02 0 0 0-.326-.941l-6.29-5.595a1.015 1.015 0 0 0-1.2-.114l-5.146 3.07a1.04 1.04 0 0 0-.412 1.307l2.71 6.037c.225.503.806.738 1.316.533l8.719-3.523c.33-.131.567-.424.629-.774m3.087 1.721 6.999 2.823c.342.146.578.467.617.838.044.375-.123.745-.434.958l-7.004 4.848a1 1 0 0 1-.572.184 1.03 1.03 0 0 1-.572-.184l-6.998-4.848a1.02 1.02 0 0 1-.44-.958c.046-.375.291-.696.64-.838l6.998-2.823c.246-.098.52-.098.766 0"
clipRule="evenodd"
/>
</svg>
)
}
export function RemixLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 61 61" {...props}>
<path
fill="currentColor"
d="M44.594 39.764c.33 4.179.33 6.137.33 8.275h-9.818c0-.466.008-.891.016-1.324.028-1.343.056-2.743-.166-5.57-.293-4.142-2.104-5.062-5.432-5.062H14.08V28.55h15.91c4.206 0 6.31-1.258 6.31-4.595 0-2.931-2.104-4.71-6.31-4.71H14.08v-7.37h17.664c9.52 0 14.252 4.428 14.252 11.5 0 5.291-3.33 8.741-7.827 9.316 3.797.748 6.017 2.875 6.425 7.072"
/>
<path
fill="currentColor"
d="M14.08 48.04v-5.616h10.383c1.733 0 2.11 1.266 2.11 2.022v3.593z"
/>
</svg>
)
}
export function SolidjsLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 62 61" {...props}>
<path
fill="currentColor"
d="M26.604 13.56c9.353-2.148 21.405 7.04 21.405 7.04l-4.094 6.904-.078-.016q.078.061.078.062a16 16 0 0 0-.722-.53c-2.732-1.93-12.692-8.369-20.683-6.534l-.118.03c-.24.059-.49.121-.705.2-1.222.4-2.186 1.039-2.86 1.828l-.022-.004 3.454-5.933q.064-.105.123-.217c.066-.122.134-.245.22-.36.663-.97 1.738-1.778 3.178-2.24.216-.079.466-.141.705-.2l.002-.001z"
/>
<path
fill="currentColor"
d="m21.455 30.312 8.945-2.924c2.699-.88 5.923-.355 8.357 1.145-.023-.016.723.478.802.567l3.385-1.332c-.244-.167-1.144-.82-1.456-1.023a48 48 0 0 0-5.086-2.886c-4.15-2.016-9.303-3.676-13.722-2.665l-.117.03c-.243.06-.45.112-.625.176l-.013.005-.012.004c-1.735.566-2.821 1.637-3.26 2.844-.438 1.2-.276 2.646.705 4.06.535.77 1.255 1.447 2.097 1.999M37.165 44.533l.004.008-.009-.001z"
/>
<path
fill="currentColor"
d="m41.109 35.964-3.25 5.838c-.058-.946-.402-1.928-1.056-2.87-2.195-3.163-7.066-4.733-10.885-3.487L12.72 39.78l.062-.191 3.846-6.934 13.998-4.574c1.742-.569 3.756-.502 5.603.09 1.848.593 3.468 1.693 4.458 3.118 1.14 1.645 1.183 3.33.422 4.675"
/>
<path
fill="currentColor"
d="M14.073 40.923a45 45 0 0 1-.827-.55l12.898-4.235c1.742-.569 3.756-.502 5.603.09 1.848.593 3.468 1.694 4.458 3.119.98 1.413 1.143 2.858.706 4.06-.44 1.206-1.526 2.277-3.261 2.843l-.013.004-.012.005c-.176.064-.382.116-.625.176l-.118.03c-4.419 1.011-9.571-.65-13.721-2.663a48 48 0 0 1-5.088-2.879"
/>
</svg>
)
}
export function StripeLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 512 214" {...props}>
<path
fill="currentColor"
d="M512 110.08c0-36.409-17.636-65.138-51.342-65.138c-33.85 0-54.33 28.73-54.33 64.854c0 42.808 24.179 64.426 58.88 64.426c16.925 0 29.725-3.84 39.396-9.244v-28.445c-9.67 4.836-20.764 7.823-34.844 7.823c-13.796 0-26.027-4.836-27.591-21.618h69.547c0-1.85.284-9.245.284-12.658m-70.258-13.511c0-16.071 9.814-22.756 18.774-22.756c8.675 0 17.92 6.685 17.92 22.756zm-90.31-51.627c-13.939 0-22.899 6.542-27.876 11.094l-1.85-8.818h-31.288v165.83l35.555-7.537l.143-40.249c5.12 3.698 12.657 8.96 25.173 8.96c25.458 0 48.64-20.48 48.64-65.564c-.142-41.245-23.609-63.716-48.498-63.716m-8.534 97.991c-8.391 0-13.37-2.986-16.782-6.684l-.143-52.765c3.698-4.124 8.818-6.968 16.925-6.968c12.942 0 21.902 14.506 21.902 33.137c0 19.058-8.818 33.28-21.902 33.28M241.493 36.551l35.698-7.68V0l-35.698 7.538zm0 10.809h35.698v124.444h-35.698zm-38.257 10.524L200.96 47.36h-30.72v124.444h35.556V87.467c8.39-10.951 22.613-8.96 27.022-7.396V47.36c-4.551-1.707-21.191-4.836-29.582 10.524m-71.112-41.386l-34.702 7.395l-.142 113.92c0 21.05 15.787 36.551 36.836 36.551c11.662 0 20.195-2.133 24.888-4.693V140.8c-4.55 1.849-27.022 8.391-27.022-12.658V77.653h27.022V47.36h-27.022zM35.982 83.484c0-5.546 4.551-7.68 12.09-7.68c10.808 0 24.461 3.272 35.27 9.103V51.484c-11.804-4.693-23.466-6.542-35.27-6.542C19.2 44.942 0 60.018 0 85.192c0 39.252 54.044 32.995 54.044 49.92c0 6.541-5.688 8.675-13.653 8.675c-11.804 0-26.88-4.836-38.827-11.378v33.849c13.227 5.689 26.596 8.106 38.827 8.106c29.582 0 49.92-14.648 49.92-40.106c-.142-42.382-54.329-34.845-54.329-50.774"
/>
</svg>
)
}
export function SvelteLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 62 61" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M28.842 13.574c4.783-3.047 11.397-1.627 14.746 3.167a10.27 10.27 0 0 1 1.754 7.766 9.7 9.7 0 0 1-1.44 3.6c1.048 2 1.4 4.293 1 6.516a9.63 9.63 0 0 1-4.354 6.454l-8.4 5.353c-4.78 3.047-11.395 1.627-14.745-3.167a10.27 10.27 0 0 1-1.754-7.765 9.7 9.7 0 0 1 1.441-3.602c-1.049-2-1.401-4.292-1-6.515a9.64 9.64 0 0 1 4.353-6.454zm-7.984 27.49a6.67 6.67 0 0 0 7.163 2.655 6.1 6.1 0 0 0 1.707-.75l8.401-5.354a5.8 5.8 0 0 0 2.619-3.883 6.18 6.18 0 0 0-1.055-4.671 6.67 6.67 0 0 0-7.163-2.655 6.1 6.1 0 0 0-1.706.75l-3.206 2.043q-.24.153-.516.226a2.01 2.01 0 0 1-2.158-.8 1.86 1.86 0 0 1-.317-1.409c.087-.483.373-.907.789-1.169l8.402-5.354a1.9 1.9 0 0 1 .516-.227 2.01 2.01 0 0 1 2.156.8c.257.364.377.806.34 1.25l-.03.311.313.095c1.181.356 2.293.912 3.287 1.643l.43.315.158-.482a6 6 0 0 0 .2-.785 6.18 6.18 0 0 0-1.055-4.671 6.67 6.67 0 0 0-7.163-2.655 6.1 6.1 0 0 0-1.707.75l-8.401 5.355a5.8 5.8 0 0 0-2.619 3.881 6.18 6.18 0 0 0 1.056 4.672 6.67 6.67 0 0 0 7.163 2.655 6.1 6.1 0 0 0 1.706-.75l3.206-2.043q.24-.152.515-.227a2.01 2.01 0 0 1 2.158.801c.29.408.405.915.318 1.408a1.75 1.75 0 0 1-.79 1.17l-8.402 5.354a1.9 1.9 0 0 1-.516.227 2.01 2.01 0 0 1-2.156-.8 1.88 1.88 0 0 1-.34-1.25l.03-.311-.313-.095a10.8 10.8 0 0 1-3.286-1.642l-.43-.316-.159.482a6 6 0 0 0-.2.785 6.18 6.18 0 0 0 1.055 4.671"
clipRule="evenodd"
/>
</svg>
)
}
export function TurbopackLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 62 62" {...props}>
<path
fill="currentColor"
d="M18.275 15.256 31 7.984l19.903 11.372v5.732L31 13.714 18.3 20.97zM16.073 39.216l12.688 7.25-4.963 2.893-12.7-7.257V19.356l4.943-2.824.027 5.722.005.003zM45.927 24.824v14.392L31 47.745v.007l-4.958 2.89L31 53.477l19.903-11.374V27.668z"
/>
<path
fill="currentColor"
d="m31 16.294-11.56 6.605 12.69 7.252 11.558-6.608zM18.312 24.834v13.081L31 45.167V32.084z"
/>
</svg>
)
}
export function VercelLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 222" {...props}>
<path fill="currentColor" d="m128 0 128 221.705H0z" />
</svg>
)
}
export function VueLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 61 61" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M43.053 13.453h7.062l-19.84 34.363-19.838-34.363h7.06l12.779 22.132zm-.877 0-11.9 20.613-11.901-20.613h7.319l4.582 7.936 4.581-7.936z"
clipRule="evenodd"
/>
</svg>
)
}
@@ -0,0 +1,591 @@
import { ComponentProps } from 'react'
export function OnePasswordLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 143 60" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M22.48 41.59c6.4 0 11.59-5.19 11.59-11.59s-5.19-11.59-11.59-11.59S10.89 23.6 10.89 30s5.19 11.59 11.59 11.59m2.218-17.766v6.656c0 .178-.07.35-.197.475l-.8.8a.135.135 0 0 0 0 .19l.8.8a.67.67 0 0 1 .197.475v3.135a.67.67 0 0 1-.672.672h-3.093a.67.67 0 0 1-.672-.672v-6.621c0-.179.071-.35.197-.476l.782-.782a.134.134 0 0 0 0-.19l-.782-.782a.67.67 0 0 1-.197-.476v-3.204c0-.372.301-.673.672-.673h3.093c.371 0 .672.302.672.673"
clipRule="evenodd"
/>
<path
fill="currentColor"
d="M86.833 26.552c.954 0 1.836-.018 2.718.018.09 0 .234.234.27.378l1.404 6.75c.054.252.108.486.198.864.09-.234.144-.324.162-.432a830 830 0 0 0 1.71-7.146c.072-.342.198-.468.558-.45.738.018 1.458.018 2.196 0 .324-.018.432.108.504.414.612 2.412 1.242 4.824 1.854 7.236.036.108.072.216.162.45.27-1.278.504-2.412.756-3.564.288-1.368.576-2.754.846-4.122.072-.324.216-.432.54-.414.792.018 1.584 0 2.43 0-.054.288-.09.522-.144.738-.774 3.168-1.566 6.336-2.34 9.504-.09.36-.234.468-.594.468-1.116-.018-2.25-.018-3.384-.018-.558-2.376-1.116-4.734-1.656-7.074-.036 0-.054-.018-.09-.018-.558 2.358-1.116 4.716-1.692 7.074h-3.744c-.918-3.528-1.782-7.056-2.664-10.656M130.049 22.575v5.093c-.27-.216-.522-.45-.774-.648-1.728-1.332-4.608-.864-5.958 1.026s-1.494 3.96-.81 6.12c.504 1.62 1.566 2.79 3.258 3.222 1.602.414 3.402-.036 4.176-1.53.018-.036.072-.054.198-.162v1.53h2.844V22.575zm-.108 9.251c.018.972-.162 1.782-.774 2.484-.756.864-2.646.9-3.348-.324-.774-1.368-.792-2.79-.018-4.14.792-1.386 2.808-1.368 3.654 0 .396.63.522 1.332.486 1.98M55.281 24.176a4.8 4.8 0 0 0-2.898-.936c-1.872-.018-3.744-.018-5.616-.018-.108 0-.198.018-.324.036v13.968h3.078v-.504c0-1.206.018-2.43 0-3.636 0-.306.09-.414.414-.414.9 0 1.8 0 2.7-.072 2.718-.216 4.338-1.944 4.41-4.644.018-1.548-.504-2.844-1.764-3.78m-3.582 5.778c-.594 0-1.188-.018-1.8 0-.288 0-.396-.09-.396-.396.018-.558 0-1.098 0-1.656 0-.54.018-1.08 0-1.62 0-.27.072-.414.378-.396.756.036 1.53.018 2.286.054.9.054 1.53.648 1.638 1.548.216 1.548-.558 2.448-2.106 2.466M67.22 37.226c-.054-2.538-.054-5.04-.198-7.524-.072-1.17-.756-2.124-1.8-2.754-1.638-.99-4.608-.972-6.246.63-.576.576-1.026 1.224-1.224 2.052.756.18 1.476.324 2.16.54.396.108.666.108.828-.36.072-.216.27-.432.45-.594.522-.468 1.134-.576 1.8-.378.612.198.936.666 1.062 1.278.18.828.018 1.026-.828 1.026-.756 0-1.53-.018-2.286 0-.468.018-.936.054-1.386.162-2.214.522-3.06 3.294-1.53 4.986 1.512 1.656 4.464 1.656 5.976-.036.09-.09.108-.234.162-.342.054.054.126.09.18.144v1.188h2.88zm-4.95-2.052a4 4 0 0 1-.936 0c-.612-.09-.99-.54-.972-1.098.018-.576.378-1.008 1.026-1.026.9-.036 1.782 0 2.7 0 .18.972-.63 1.962-1.818 2.124M108.605 26.282c-3.078-.054-5.454 2.304-5.508 5.454-.054 3.168 2.214 5.652 5.166 5.778 3.132.126 5.778-2.088 5.796-5.58.018-3.294-2.358-5.652-5.454-5.652m1.8 7.56c-1.008 1.494-3.042 1.368-3.798-.27-.234-.504-.306-1.098-.468-1.674.162-.576.234-1.17.468-1.692.324-.756.954-1.224 1.782-1.314.864-.072 1.53.306 2.016 1.026.702 1.062.702 2.88 0 3.924M76.94 29.432c-.846.162-1.692.306-2.52.45-.072.018-.216-.162-.27-.27-.504-1.008-1.53-1.44-2.61-1.062-.396.144-.684.378-.72.828-.054.468.216.756.612.882.972.306 1.962.522 2.916.846.63.216 1.296.468 1.8.864 1.098.882 1.224 2.52.432 3.726-.648.972-1.566 1.494-2.7 1.692-1.728.306-3.402.18-4.896-.846-.684-.468-1.08-1.134-1.242-1.998.81-.198 1.584-.396 2.358-.576.09-.018.288.09.324.198.612 1.224 2.16 1.296 3.024 1.008.396-.126.702-.396.702-.882.018-.486-.324-.666-.684-.774-.882-.252-1.782-.486-2.664-.738-.324-.09-.648-.216-.972-.342-1.098-.45-1.8-1.206-1.872-2.448-.072-1.206.324-2.196 1.35-2.898.9-.63 1.926-.792 2.988-.81 1.314 0 2.574.216 3.564 1.17.54.54.9 1.17 1.08 1.98M77.57 34.526c.792-.198 1.548-.396 2.304-.558.108-.018.324.072.378.162.576 1.134 1.944 1.314 2.934 1.044.504-.126.792-.486.81-.9.018-.504-.342-.684-.72-.792-.882-.27-1.782-.486-2.682-.72-.324-.09-.648-.216-.972-.342-1.08-.45-1.764-1.206-1.854-2.412-.072-1.188.306-2.178 1.296-2.88.9-.648 1.944-.828 3.024-.828 1.314 0 2.556.216 3.564 1.17.54.504.918 1.188 1.098 1.98-.666.126-1.35.216-1.998.378-.45.108-.684.036-.918-.432-.468-.918-1.728-1.242-2.664-.774-.324.162-.522.432-.522.81s.198.648.522.756c.756.252 1.548.45 2.304.702.666.216 1.368.378 1.98.702 1.512.81 1.908 2.646.954 4.086-.63.972-1.566 1.494-2.682 1.692-1.728.306-3.402.18-4.896-.846-.684-.45-1.08-1.116-1.26-1.998M40.395 37.208V26.75h-2.052v-2.466c.432-.072.882-.126 1.314-.234.54-.126.99-.396 1.332-.846.072-.09.18-.198.27-.198.738-.018 1.494 0 2.268 0v14.202zM117.95 26.588v1.71c.036 0 .054.018.09.018.126-.198.234-.414.36-.612.684-1.098 2.034-1.656 3.258-1.368.324.072.522.18.414.612a11.6 11.6 0 0 0-.36 2.07c-.036.45-.252.414-.576.414-.54-.018-1.098 0-1.638.09-.684.108-1.08.576-1.188 1.242-.072.414-.09.828-.09 1.242-.018 1.566 0 3.15 0 4.716v.522h-3.042V26.606c.882-.018 1.782-.018 2.772-.018"
/>
</svg>
)
}
export function BetasharesLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 149 60" {...props}>
<g fill="currentColor" fillRule="evenodd" clipPath="url(#a)" clipRule="evenodd">
<path d="M36.341 30.003c0 7.091-5.748 12.84-12.84 12.84-7.09 0-12.84-5.749-12.84-12.84s5.75-12.84 12.84-12.84c7.092 0 12.84 5.749 12.84 12.84m-15.725-5.14a.085.085 0 0 0-.128.075v10.139a.086.086 0 0 0 .128.074l8.78-5.07a.086.086 0 0 0 0-.148zm1.532-.784c-.078 0-.107.105-.038.145h-.002l7.044 4.067c.07.04.146-.037.107-.105l-2.294-3.99a.23.23 0 0 0-.201-.117zM19.042 34.09c.04.07.146.042.146-.038v-8.103c0-.08-.105-.106-.146-.038l-2.284 3.973a.23.23 0 0 0 0 .233zm10.102-2.362c.07-.04.147.037.107.105l-.001.002-2.286 3.973a.23.23 0 0 1-.202.117h-4.597c-.08 0-.107-.105-.04-.145zm2.437-2.114a.78.78 0 0 1 0 .774l-3.706 6.446a.78.78 0 0 1-.674.39h-7.407a.78.78 0 0 1-.673-.39l-3.706-6.446a.78.78 0 0 1 0-.774l3.706-6.446a.78.78 0 0 1 .673-.39h7.407a.78.78 0 0 1 .674.39zM43.308 32.33c0-1.697 1.253-3.045 3.037-3.045s3.038 1.348 3.038 3.045-1.275 3.066-3.038 3.066c-1.764 0-3.038-1.37-3.038-3.066m3.568 5.17c2.89 0 4.865-2.288 4.865-5.17s-1.976-5.15-4.865-5.15c-1.402 0-2.782.634-3.505 1.758V22.95h-2.358v14.304h2.23v-1.758c.68 1.288 2.168 2.003 3.633 2.003zm7.97-6.336c.362-1.308 1.338-2.125 2.783-2.125s2.465.817 2.805 2.125zM57.8 37.56c2.082 0 3.91-.879 4.823-2.84v-.042c-.638.041-1.593-.389-1.976-.899l-.02.02c-.383 1.023-1.402 1.78-2.783 1.78-1.657 0-2.91-1.044-3.123-2.739h7.966c.382-3.085-1.657-5.722-5.036-5.722-2.996 0-5.163 2.33-5.163 5.21s2.21 5.232 5.312 5.232m5.388-8.112h1.38v7.806h2.381v-7.806h1.509v-2.044h-1.509v-3.148h-2.38v3.148h-1.381zm8.06 2.882c0-1.697 1.253-3.045 3.037-3.045s3.06 1.369 3.06 3.045-1.254 3.066-3.06 3.066-3.038-1.37-3.038-3.066m2.528 5.15c1.381 0 2.89-.675 3.569-1.942v1.716h2.294v-9.85h-2.294v1.716c-.68-1.266-2.188-1.941-3.569-1.941-2.91 0-4.887 2.288-4.887 5.15 0 2.86 1.976 5.149 4.887 5.149zm11.263.081c2.338 0 3.93-1.43 3.93-3.29s-1.487-2.677-3.059-2.984l-.957-.184c-.85-.184-1.487-.409-1.487-1.062 0-.572.552-1.103 1.424-1.103.956 0 1.7.47 1.932 1.471h.02c.404-.47 1.361-.838 1.997-.756l.021-.021c-.383-1.39-1.763-2.533-3.91-2.533-2.146 0-3.782 1.39-3.782 3.066 0 1.512 1.02 2.411 3.08 2.84l.999.204c.786.184 1.402.43 1.402 1.145 0 .717-.552 1.288-1.593 1.288-1.274 0-2.188-.817-2.358-1.84l-.021-.02c-.404.47-1.36.817-1.997.736l-.02.02c.382 1.84 2.082 3.025 4.376 3.025zm5.077-.307h2.359v-5.599c0-1.43 1.02-2.37 2.422-2.37 1.401 0 2.294.899 2.294 2.288v5.681h2.358v-6.518c0-2.084-1.657-3.576-3.846-3.576-1.296 0-2.57.572-3.228 1.736V22.95h-2.359zm12.922-4.924c0-1.697 1.253-3.045 3.037-3.045s3.06 1.369 3.06 3.045-1.253 3.066-3.06 3.066c-1.806 0-3.037-1.37-3.037-3.066m2.528 5.15c1.381 0 2.89-.675 3.569-1.942v1.716h2.294v-9.85h-2.294v1.716c-.679-1.266-2.188-1.941-3.569-1.941-2.91 0-4.887 2.288-4.887 5.15 0 2.86 1.976 5.149 4.887 5.149zm7.716-7.378v7.152h2.358v-4.372c0-1.716.531-3.31 2.252-3.31.51 0 .956.122 1.189.306h.021c.021-.633.574-1.553 1.126-1.9v-.041c-.383-.429-1.062-.694-1.976-.694-1.296 0-2.21.777-2.677 1.839v-1.676h-2.295v2.698zm8.647 1.062c.36-1.308 1.338-2.125 2.782-2.125 1.445 0 2.465.817 2.805 2.125zm2.953 6.396c2.082 0 3.909-.879 4.823-2.84v-.042c-.638.041-1.593-.389-1.976-.899l-.02.02c-.383 1.023-1.402 1.78-2.783 1.78-1.656 0-2.91-1.044-3.123-2.739h7.966c.382-3.085-1.657-5.722-5.036-5.722-2.996 0-5.162 2.33-5.162 5.21s2.209 5.232 5.311 5.232m9.863.001c2.337 0 3.93-1.43 3.93-3.29s-1.487-2.677-3.06-2.984l-.956-.184c-.85-.184-1.488-.409-1.488-1.062 0-.572.552-1.103 1.424-1.103.957 0 1.7.47 1.933 1.471h.02c.404-.47 1.36-.838 1.997-.756l.02-.021c-.382-1.39-1.763-2.533-3.909-2.533s-3.782 1.39-3.782 3.066c0 1.512 1.02 2.411 3.08 2.84l.998.204c.787.184 1.402.43 1.402 1.145 0 .717-.552 1.288-1.593 1.288-1.274 0-2.188-.817-2.358-1.84l-.021-.02c-.403.47-1.36.817-1.996.736l-.021.02c.383 1.84 2.082 3.025 4.377 3.025z" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M10.59 17.04h128.13v25.92H10.59z" />
</clipPath>
</defs>
</svg>
)
}
export function BoltLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 114 60" {...props}>
<path
fill="currentColor"
d="M54.241 45.251c-8.584 0-12.785-4.931-12.785-11.141S47.3 20.046 55.885 20.046c8.584 0 12.785 4.931 12.785 11.141s-5.845 14.064-14.429 14.064m.365-7.854c2.923 0 4.932-2.74 4.932-5.662s-1.461-3.653-4.018-3.653c-2.558 0-4.932 2.74-4.932 5.662 0 2.923 1.461 3.653 4.018 3.653m23.197 7.306h-8.95l7.306-33.242h8.95l-7.306 33.06z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M27.76 45.251c-2.739 0-5.479-.913-6.94-3.105l-.548 2.557L10.227 50l1.095-5.297 7.306-33.242h8.95l-2.557 11.69c2.01-2.192 4.018-3.105 6.575-3.105 5.48 0 8.95 3.47 8.95 10.045 0 6.576-4.2 15.16-12.785 15.16m3.471-13.333c0 3.105-2.192 5.48-5.114 5.48s-3.105-.549-4.018-1.645l1.46-6.027c1.097-1.096 2.193-1.644 3.654-1.644 2.192 0 4.018 1.644 4.018 4.018z"
clipRule="evenodd"
/>
<path
fill="currentColor"
d="M94.414 45.251c-5.115 0-8.95-1.826-8.95-6.027s0-1.279.182-1.827l2.01-8.95h-4.019l1.827-7.67h4.018l1.461-6.576L100.99 10l-1.096 4.2-1.461 6.576h4.931l-1.826 7.672h-4.932l-1.278 5.844v1.096c0 1.096.73 2.01 2.192 2.01 1.46 0 1.095 0 1.278-.183v7.123c-.913.73-2.557.913-4.2.913z"
/>
</svg>
)
}
export function BrthrsLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 130 40" {...props}>
<title>brthrs-logotype-color</title>
<defs>
<polygon id="path-1" points="0 0 130 0 130 40 0 40"></polygon>
</defs>
<g id="brthrs-logotype-color" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<mask id="mask-2" fill="white">
<use xlinkHref="#path-1"></use>
</mask>
<g id="Clip-2"></g>
<path
d="M119.768234,32 C117.898023,32 116.681663,31.1207404 116,30.0152264 C119.41895,25.6778708 121.631357,19 121.631357,19 C122.4409,19 124,24.3952982 124,27.528052 C124,29.7130897 122.63122,32 119.768234,32 Z M125.722294,10.5495228 L117.486497,10.5495228 C118.056172,14.9750306 114.232541,23.6322092 112.742226,26.8614646 L112.733409,26.8431644 C112.733409,26.8431644 110.422135,32.3914833 107.248266,32.3914833 C106.078546,32.3914833 104.972505,31.5571406 105.13317,29.6775217 C105.286242,27.8865142 106.602912,24.0398692 107.021473,22.679397 C108.576936,17.6227745 106.671733,13.5480999 100.056547,13.5480999 C100.056547,13.5480999 100.055567,13.5483407 100.055322,13.5483407 C100.055567,13.5476184 100.056547,13.5442473 100.056547,13.5442473 C100.056547,13.5442473 100.393552,11.3723075 100.206925,10.5495228 L94.3624904,10.5495228 C94.6096108,15.7665125 90.012289,32.4052084 85.1651527,32.4052084 C83.8590147,32.4052084 82.820815,31.7040234 83.4487801,28.8419751 C83.6846344,27.7666 85.7218472,19.2103132 85.7218472,17.5062314 C85.7218472,14.6735596 84.1149521,13.2208642 80.9450019,13.2208642 C77.9229811,13.2208642 75.629096,15.0294495 74.49244,16.0511281 L74.8850407,14.2603613 C75.8590723,10.0183366 76.649172,6.52637762 76.9761351,5.0113172 L71.1287611,5.0113172 C70.9053975,6.00939817 70.0876224,9.44332643 69.2044546,13.3463166 L69.2039648,13.3463166 C69.2039648,13.3463166 69.1983317,13.3725629 69.1878003,13.4207212 C69.106733,13.7787782 69.0254209,14.1404471 68.9436189,14.5047648 C68.9218213,14.6025261 68.9071263,14.6925822 68.8887576,14.7864909 C68.4295398,16.88523 67.5304524,20.9943378 66.6188744,25.1590684 C64.8963788,27.8997577 61.5782543,32.4013557 58.3620149,32.4013557 C55.9243641,32.4013557 56.1866694,29.1451316 56.5770658,27.3818151 C57.1805392,24.6562956 57.8631207,21.5823507 58.5285579,18.5859407 L62.3720277,18.5859407 L63.5615856,13.2283288 L59.7186057,13.2242353 C60.5471572,9.49052157 61.2334123,6.38792248 61.5326999,5.00144475 L55.6564258,5 C55.4286538,6.01782587 54.6929255,9.33472944 53.832535,13.2182155 L50.328764,13.2146036 L49.139206,18.5859407 L52.6434669,18.5859407 C51.9907652,21.5322661 51.3623103,24.370476 50.9057865,26.4333372 C50.4125253,27.2614194 47.2070622,32.4013557 43.5874459,32.4013557 C42.4179711,32.4013557 41.3685052,31.5571406 41.5289253,29.6775217 C41.6819979,27.8865142 42.9989123,24.0398692 43.417474,22.679397 C44.9726918,17.6227745 43.3596738,13.5480999 38.1189572,13.5480999 C38.1189572,13.5480999 38.7123892,11.6932826 38.6663449,10.5495228 L32.7121875,10.5533755 C32.5836065,11.9675442 32.0330349,16.42893 28,19.2404121 L31.3394322,23.6526764 C31.3394322,23.6526764 33.9717914,22.04756 35.5911773,18.9781901 C36.8270243,19.1869564 38.3704862,20.0039621 37.5336075,22.4386055 C37.5336075,22.4386055 35.4831692,28.5484502 35.4510852,31.5585853 C35.4008774,36.2740062 38.9119959,38 41.9883881,38 C46.1311454,38 48.8896364,35.9824076 50.4762035,34.2903654 C51.2378929,36.359728 53.0706007,38 56.7394452,38 C60.0590392,38 62.6953171,36.3435949 64.5534962,34.5973746 C64.1704473,36.3479292 63.9145098,37.5174537 63.8814462,37.6689116 L69.7626186,37.6689116 C70.0988885,36.1572223 71.6014493,28.9659827 72.6129532,24.6170466 C73.4353817,21.0812635 76.1363175,18.6762375 78.5445783,18.6762375 C79.3202279,18.6762375 79.6075146,19.5418831 79.4608098,20.2103205 C79.2905931,20.9856693 78.9626503,22.8238719 78.3648099,25.2276939 C77.6214892,28.2173618 77.2210513,30.6587473 77.2210513,32.2210029 C77.2210513,36.5952221 80.3843888,38 83.6701843,38 C92.6436686,38 97.1067763,24.3538614 98.5287596,18.9175107 C99.74036,18.9300318 102.1376,19.5300844 101.138097,22.4386055 C101.138097,22.4386055 99.0876584,28.5484502 99.0555744,31.5585853 C99.0053665,36.2740062 102.51624,38 105.592387,38 C109.126773,38 111.53185,36.5415256 113.060127,35.0702892 C114.076039,36.2198281 116.303552,38 120.366956,38 C126.607421,38 130,33.7459357 130,27.7661184 C130,21.9066969 126.205513,15.2346039 125.722294,10.5495228 L125.722294,10.5495228 Z"
id="Fill-1"
fill="#FFFFFF"
mask="url(#mask-2)"
></path>
<path
d="M11,33.9811595 C11.425622,32.109806 12.5859338,27.0088547 13.7228352,22 C16.5998306,22.3743664 21,23.591715 21,27.5363166 C21,32.810218 16.086793,34.1830542 11,33.9811595 Z M22.5070236,19.5325178 C26.4719736,17.2493915 29,13.4510219 29,8.81000891 C29,0.997783594 21.3553589,0 18.716838,0 C8.23196781,0 1.30739528,6.99914407 0.750967456,7.63045475 L3.96791626,11.9724259 C4.50358001,11.4229608 10.1929371,5.02898568 17.9752638,5.02898568 C20.6676725,5.02898568 23.2008899,6.23003958 23.2008899,9.65069778 C23.2008899,11.9343127 21.584011,15.2538244 15.1137762,17.1560631 C15.821238,13.9425843 16.3979355,11.3157064 16.597419,10.3873083 L10.6608705,10.3909731 C10.1936787,12.489397 7.01801088,26.9159698 5.59567159,33.3785976 C2.72627389,32.4355406 1.16229394,31.1883111 1.16229394,31.1883111 L0,36.5480996 C0,36.5480996 4.32337749,39.5096894 10.3533644,39.9450593 C17.6909937,40.4749791 27.1307387,37.2275404 27.1307387,27.7603234 C27.1307387,23.5236522 24.9893196,21.0160002 22.5070236,19.5325178 L22.5070236,19.5325178 Z"
id="Fill-3"
fill="#FFFFFF"
mask="url(#mask-2)"
></path>
</g>
</svg>
)
}
export function ChatbaseLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 148 60" {...props}>
<g fill="currentColor" clipPath="url(#a)">
<path
fillRule="evenodd"
d="M17.702 16.5h13.385c3.905 0 7.071 3.105 7.071 6.936v13.128c0 3.83-3.166 6.936-7.071 6.936H17.702c-3.906 0-7.072-3.105-7.072-6.936V23.436c0-3.83 3.166-6.936 7.072-6.936m9.817 11.51h4.452q-.127-1.484-.718-2.633a6 6 0 0 0-1.563-1.935 6.8 6.8 0 0 0-2.257-1.205q-1.287-.411-2.818-.411-2.178 0-3.946.94-1.767.931-2.81 2.758-1.041 1.818-1.041 4.476 0 2.641 1.018 4.468 1.025 1.826 2.786 2.766 1.768.94 3.993.94 1.776 0 3.118-.536t2.249-1.414a6.7 6.7 0 0 0 1.413-1.927q.497-1.05.576-2.06l-4.451-.03q-.08.513-.308.924-.222.405-.576.692-.355.288-.837.443a3.7 3.7 0 0 1-1.09.147q-1.065 0-1.822-.49-.758-.496-1.16-1.475-.396-.987-.395-2.448 0-1.383.387-2.37.394-.987 1.152-1.515.766-.529 1.87-.529.631 0 1.129.18.497.17.845.496.354.32.552.762.205.443.253.987"
clipRule="evenodd"
/>
<path d="M59.893 27.327h-2.5a3.6 3.6 0 0 0-.537-1.364 3.9 3.9 0 0 0-.962-1 4.2 4.2 0 0 0-1.274-.62 5.1 5.1 0 0 0-1.482-.209q-1.418 0-2.54.69-1.113.69-1.763 2.022-.64 1.334-.64 3.255 0 1.935.64 3.27.65 1.332 1.771 2.014 1.123.681 2.524.682.777 0 1.474-.202.705-.21 1.274-.612.57-.403.962-.984.4-.588.552-1.348l2.5.008a6.3 6.3 0 0 1-.777 2.154 6 6 0 0 1-1.466 1.689 6.7 6.7 0 0 1-2.035 1.092 7.8 7.8 0 0 1-2.5.388q-2.13 0-3.798-.977-1.666-.984-2.628-2.812-.954-1.829-.954-4.363 0-2.541.962-4.362.96-1.829 2.628-2.805 1.667-.983 3.79-.983 1.306-.001 2.436.364 1.138.355 2.043 1.053a5.9 5.9 0 0 1 1.498 1.69q.593.99.801 2.27M63.705 30.968v7.067h-2.396V22.166h2.364v5.904h.152q.433-.96 1.322-1.526t2.324-.566q1.266 0 2.21.504a3.46 3.46 0 0 1 1.475 1.503q.53.992.53 2.48v7.57h-2.397v-7.291q0-1.31-.697-2.03-.697-.729-1.939-.729-.85 0-1.522.349a2.56 2.56 0 0 0-1.05 1.023q-.375.666-.376 1.611M77.11 38.298q-1.17 0-2.115-.418a3.53 3.53 0 0 1-1.499-1.232q-.544-.806-.544-1.976 0-1.007.4-1.658a2.9 2.9 0 0 1 1.082-1.03 5.8 5.8 0 0 1 1.522-.574q.842-.194 1.715-.294l1.795-.202q.69-.085 1.001-.27.313-.187.313-.605v-.054q0-1.015-.593-1.573-.585-.558-1.747-.558-1.21 0-1.907.519-.69.51-.953 1.139l-2.252-.496q.4-1.085 1.17-1.751a4.8 4.8 0 0 1 1.787-.976 7.2 7.2 0 0 1 2.123-.31 7.7 7.7 0 0 1 1.563.17q.832.163 1.554.605.729.441 1.194 1.262.465.814.465 2.116v7.903h-2.34v-1.627h-.096q-.232.45-.697.883-.465.435-1.194.72-.729.288-1.747.287m.521-1.86q.993 0 1.698-.379.714-.38 1.082-.992.377-.62.377-1.325v-1.534q-.129.124-.497.233a8 8 0 0 1-.825.178l-.906.132q-.44.054-.737.093-.697.084-1.274.286-.569.202-.913.581-.337.372-.337.992 0 .86.657 1.302.657.434 1.675.434M90.766 26.134v1.86h-6.722v-1.86zm-4.92-2.852h2.396V34.54q0 .675.208 1.015.21.333.537.458.337.116.73.116.288 0 .504-.04.216-.038.337-.061l.432 1.914a4 4 0 0 1-.593.155 5 5 0 0 1-.961.093 4.3 4.3 0 0 1-1.763-.326 3 3 0 0 1-1.322-1.053q-.505-.713-.505-1.79zM92.518 38.035V22.166h2.396v5.897h.144q.21-.372.601-.86.393-.489 1.09-.853.698-.372 1.843-.372 1.49 0 2.66.729 1.17.727 1.835 2.1.673 1.371.673 3.3 0 1.93-.665 3.309-.665 1.372-1.827 2.115-1.162.736-2.652.736-1.122 0-1.835-.364-.705-.365-1.106-.852a6.4 6.4 0 0 1-.617-.868h-.2v1.852zm2.348-5.951q0 1.255.377 2.2.376.946 1.09 1.48.713.527 1.746.527 1.074 0 1.795-.55.722-.558 1.089-1.51.377-.954.377-2.147 0-1.178-.368-2.115-.361-.938-1.09-1.48-.72-.543-1.803-.543-1.041 0-1.763.52-.713.519-1.081 1.449-.369.93-.369 2.169M108.678 38.298q-1.17 0-2.115-.418a3.54 3.54 0 0 1-1.499-1.232q-.544-.806-.544-1.976 0-1.007.4-1.658a2.9 2.9 0 0 1 1.082-1.03 5.8 5.8 0 0 1 1.522-.574q.842-.194 1.715-.294l1.795-.202q.689-.085 1.001-.27.313-.187.313-.605v-.054q0-1.015-.593-1.573-.585-.558-1.747-.558-1.21 0-1.907.519-.689.51-.953 1.139l-2.252-.496q.4-1.085 1.17-1.751a4.8 4.8 0 0 1 1.787-.976 7.2 7.2 0 0 1 2.123-.31q.737 0 1.563.17.833.163 1.554.605.729.441 1.194 1.262.464.814.465 2.116v7.903h-2.34v-1.627h-.096q-.233.45-.697.883-.465.435-1.194.72-.729.288-1.747.287m.521-1.86q.993 0 1.699-.379.713-.38 1.081-.992a2.5 2.5 0 0 0 .377-1.325v-1.534q-.129.124-.497.233-.36.1-.825.178l-.906.132q-.44.054-.737.093a6 6 0 0 0-1.274.286 2.2 2.2 0 0 0-.913.581q-.337.372-.337.992 0 .86.657 1.302.657.434 1.675.434M125.811 29.04l-2.171.371a2.5 2.5 0 0 0-.433-.767 2.14 2.14 0 0 0-.785-.596q-.498-.233-1.242-.233-1.018 0-1.699.442-.681.433-.681 1.123 0 .597.457.961t1.474.597l1.955.434q1.699.38 2.532 1.17.834.789.833 2.053 0 1.07-.641 1.906-.633.829-1.77 1.302-1.13.472-2.62.472-2.068 0-3.374-.852-1.306-.86-1.602-2.44l2.315-.342q.217.876.89 1.325.672.442 1.754.442 1.178 0 1.883-.473.706-.48.706-1.17a1.2 1.2 0 0 0-.433-.937q-.425-.38-1.306-.574l-2.084-.441q-1.722-.38-2.547-1.21-.818-.828-.818-2.099 0-1.053.609-1.844.609-.79 1.683-1.232 1.074-.45 2.46-.45 1.995 0 3.141.838a3.9 3.9 0 0 1 1.514 2.223M132.636 38.275q-1.82 0-3.133-.751a5.16 5.16 0 0 1-2.019-2.131q-.705-1.38-.705-3.231 0-1.83.705-3.223.713-1.395 1.987-2.178 1.282-.782 2.997-.782 1.041 0 2.019.333.977.333 1.754 1.046.778.712 1.226 1.852.45 1.13.449 2.75v.822h-9.783v-1.736h7.435q0-.915-.384-1.62a2.86 2.86 0 0 0-1.082-1.123q-.689-.41-1.618-.41-1.01 0-1.763.48a3.26 3.26 0 0 0-1.154 1.24 3.5 3.5 0 0 0-.401 1.65v1.356q0 1.193.433 2.03.441.837 1.226 1.278.785.434 1.835.434a4 4 0 0 0 1.242-.186q.561-.193.969-.573.41-.38.625-.937l2.268.395a3.94 3.94 0 0 1-.978 1.697q-.696.72-1.754 1.123-1.05.395-2.396.395" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M10.63 16.5h127.286v27H10.63z" />
</clipPath>
</defs>
</svg>
)
}
export function CoComLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 92 60" {...props}>
<g clipPath="url(#a)">
<path
fill="currentColor"
d="M81.632 29.3c0-5.052-2.43-10.113-6.03-13.59-3.068-2.967-7.152-5.09-11.425-5.534-3.876-.397-7.76 0-11.327 1.633a20 20 0 0 0-8.218 6.876 20.2 20.2 0 0 0-2.979 6.76c-.363 1.575-.476 3.24-.69 4.847-.092.673-.164 1.35-.31 2.011a10.74 10.74 0 0 1-6.701 7.713c-4.633 1.702-9.942-.266-12.528-4.422a10.702 10.702 0 0 1 7.495-16.233c1.298-.195 2.62-.15 3.902.135.789.174 1.556.436 2.287.782.098.047.524.32.593.283.069-.036.12-.225.152-.28l3.775-6.909.535-.978a20.9 20.9 0 0 0-16.248-1.345 20.18 20.18 0 0 0-12.593 12.763 19.866 19.866 0 0 0 10.306 24 21.8 21.8 0 0 0 8.178 2.036 20.03 20.03 0 0 0 15.113-5.796c.364-.364.727-.749 1.065-1.142 3.637 4.197 9.131 6.96 14.728 6.997 3.153.07 6.283-.55 9.17-1.819a20.26 20.26 0 0 0 8.048-6.581A19.95 19.95 0 0 0 81.632 29.3m-9.433 1.72a10.777 10.777 0 0 1-11.265 9.67 10.774 10.774 0 1 1 11.265-9.67"
/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M10.364 10h71.273v40H10.364z" />
</clipPath>
</defs>
</svg>
)
}
export function FigmaLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 166 60" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M45.155 10h16.479q.02 1.977-.039 3.945-6.198.03-12.398.02v8.317q4.761-.01 9.522.02a49 49 0 0 1 0 3.886q-4.761.03-9.522.02.01 7.325-.02 14.651-1.98.018-4.022 0zM30.853 10.04q4.604.73 5.81 5.246.727 5.21-3.925 7.579l-.039-.04a5.1 5.1 0 0 1-1.846.467h-.078q-.978.038-1.943.078h-5.247a571 571 0 0 1-.077-13.292q3.171-.014 6.315-.039l1.01.078a.1.1 0 0 0 .02-.078M23.43 23.331q-3.751.004-7.346 0h-.038a.1.1 0 0 0-.04-.058 5.7 5.7 0 0 1-.97-.195 18 18 0 0 1-1.361-.485q-2.18-1.169-3.07-3.46a.11.11 0 0 1 .038-.096 5 5 0 0 1-.388-1.769.56.56 0 0 0-.117-.31v-.506q.09-.105.117-.253.039-1.11.486-2.157l.524-1.049q1.326-1.985 3.615-2.681a.11.11 0 0 1 .097.038 2.8 2.8 0 0 1 1.05-.194q.482-.084.97-.117h6.22q.124.036.252.078-.018 6.608-.04 13.214"
clipRule="evenodd"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M30.853 10.04a.1.1 0 0 1-.02.077l-1.01-.078q-3.144.025-6.315.04-.039 6.653.078 13.291h-.118q0-.039-.038-.039.021-6.605.039-13.214a7 7 0 0 0-.253-.078h-6.218q-.489.033-.972.117a2.8 2.8 0 0 0-1.049.194.11.11 0 0 0-.097-.038 7.3 7.3 0 0 1 1.554-.292q7.21-.049 14.42.02M68.396 11.089q2.228-.05 2.895 2.06.277 2.112-1.651 3.012-2.031.599-3.207-1.186-.877-1.865.603-3.284a3.2 3.2 0 0 1 1.36-.602M11.265 12.993l-.524 1.05a6 6 0 0 0-.486 2.157.5.5 0 0 1-.117.252q.074-1.328.603-2.565.207-.487.524-.894M10.138 17.346q.082.027.078.116-.09-.017-.078-.116"
clipRule="evenodd"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M10.138 16.957a.56.56 0 0 1 .117.311q.032.91.388 1.768a.11.11 0 0 0-.038.098 5.3 5.3 0 0 1-.39-1.672q.006-.089-.077-.116zM85.069 19.522q3.542-.109 6.043 2.371.01-.87.059-1.73 2-.028 4.003-.019.01 9.872-.02 19.743-.559 5.993-5.888 8.687-4.269 1.9-8.589 0a9 9 0 0 1-1.166-.68l-.039-.078a224 224 0 0 1 2.002-3.42q2.755 1.86 5.849.719 3.288-1.495 3.79-5.072.009-1.665.058-3.323-.04-.039-.078 0-2.895 2.775-6.918 2.293-4.296-.706-6.471-4.489-1.809-3.474-1.166-7.229.69-3.57 3.44-5.927a8.73 8.73 0 0 1 5.09-1.846m.233 3.965q3.477-.093 5.188 2.992 1.009 2.12.467 4.43-.821 2.838-3.556 3.945-3.36.93-5.655-1.73-2.142-2.931-.816-6.334 1.344-2.81 4.372-3.303M110.059 19.522q3.576-.068 5.849 2.682.252.397.525.777 2.814-3.967 7.656-3.362 3.988.878 5.422 4.723.356 1.003.505 2.06.096 7.227.039 14.457h-3.984q.01-6.608-.019-13.214-.255-3.227-3.362-4.1-2.797-.39-4.372 1.923a4.4 4.4 0 0 0-.622 1.71q-.029 6.84-.019 13.68h-4.042q.01-6.761-.02-13.524-.486-3.05-3.478-3.79-2.781-.333-4.334 2.041a4.7 4.7 0 0 0-.582 1.827q-.03 6.724-.02 13.447h-4.003q-.007-10.344-.039-20.657a71 71 0 0 1 4.042-.058q-.01.914.02 1.827 1.856-2.351 4.838-2.449M143.832 19.522q4.248-.109 7.268 2.876.048-1.126.039-2.215 2-.058 4.003-.039V40.86q-2.002.02-4.003-.039a48 48 0 0 0-.039-2.215q-2.265 2.259-5.402 2.779-6.441.703-9.969-4.722-2.443-4.382-1.322-9.095 1.007-4.027 4.373-6.432a10.25 10.25 0 0 1 5.052-1.613m.233 3.965q3.476-.129 5.616 2.604 2.4 3.48.816 7.462-1.608 3.36-5.266 3.944-3.456.196-5.616-2.507-2.06-2.888-1.282-6.373.92-3.304 4.061-4.722.364-.126.738-.233.495-.061.933-.175M66.687 20.144h4.041q.01 10.338-.019 20.676a104 104 0 0 1-4.023.039zM13.675 22.593q.667.27 1.36.485.477.14.972.195a.1.1 0 0 1 .038.058 8 8 0 0 1-1.904-.486 4 4 0 0 0-.466-.252M16.084 23.331q3.595.005 7.346 0 .039 0 .039.039v13.253h-6.763a.56.56 0 0 1-.272 0 1.5 1.5 0 0 0-.272 0 4 4 0 0 1-.563-.078l-.04.039a6 6 0 0 0-.33-.117.43.43 0 0 0-.272-.077 6 6 0 0 1-2.37-1.244.26.26 0 0 0-.117-.117 4.5 4.5 0 0 1-.855-.855q.005-.09-.078-.116a5.2 5.2 0 0 1-1.088-2.177.44.44 0 0 0-.078-.194.36.36 0 0 1-.039-.194 1.6 1.6 0 0 0-.077-.35 1.1 1.1 0 0 1-.04-.389q.03-.168-.077-.272v-1.01q.104-.087.078-.233a6.4 6.4 0 0 1 .738-2.449q.082-.027.078-.116l.039-.078q.082-.027.078-.117.102-.185.233-.35.082-.027.078-.116 1.757-2.326 4.624-2.682M23.469 23.37h.039q-.02 2.79.039 5.558a.23.23 0 0 1-.078.155z"
clipRule="evenodd"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M30.775 23.292q.02.078.04 0h.038q3.762.616 5.422 4.081 1.385 3.665-.933 6.801-2.445 2.791-5.985 2.488-3.77-.609-5.402-4.042a26 26 0 0 1-.35-1.088 1.8 1.8 0 0 1-.058-.428q.021-.128-.078-.194v-1.827a.23.23 0 0 0 .078-.155q.364-2.41 2.215-4.023a6.8 6.8 0 0 1 3.42-1.516q-.174-.028-.35-.019.964-.04 1.943-.078M11.46 26.013q.005.09-.078.116-.005-.089.078-.116M11.149 26.48q.005.089-.078.116-.005-.09.078-.117M11.032 26.674q.005.089-.078.116-.005-.09.078-.116M10.216 29.239q.025.146-.078.233-.027-.15.078-.234M10.138 30.482q.108.105.078.272a.09.09 0 0 1-.058-.039.7.7 0 0 1-.02-.233M10.255 31.143q.057.165.078.35a.5.5 0 0 1-.078-.35M10.372 31.687q.06.084.077.194-.1-.066-.077-.194M11.537 34.058q.082.027.078.116-.082-.027-.078-.116M12.47 35.03q.077.038.117.116-.117 0-.117-.117M14.958 36.39a.43.43 0 0 1 .272.077.43.43 0 0 1-.273-.077M16.434 36.623a.5.5 0 0 1-.272.039v-.04q.119-.012.272 0"
clipRule="evenodd"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M16.434 36.623a.56.56 0 0 0 .272 0l6.743.078q.03 3.478.02 6.956.021.397-.078.778a1.9 1.9 0 0 0-.117.466q-.805 3.09-3.692 4.489a2.6 2.6 0 0 1-.583.175q-.189.032-.35.116-.21.024-.388.117a.28.28 0 0 1-.233.039 1.3 1.3 0 0 0-.311.02.09.09 0 0 0-.04.057h-.271a.34.34 0 0 0-.272.078h-.467a.09.09 0 0 0-.038-.058 2 2 0 0 0-.39-.02h-.038a6 6 0 0 0-.777-.116h-.04a1.1 1.1 0 0 0-.388-.117.36.36 0 0 1-.194-.039.23.23 0 0 0-.194-.077 1.1 1.1 0 0 1-.467-.156.3.3 0 0 0-.116-.078 4.8 4.8 0 0 1-1.4-.855.43.43 0 0 0-.233-.194 5.8 5.8 0 0 1-1.476-1.904.6.6 0 0 0-.078-.156 3 3 0 0 1-.389-1.01.5.5 0 0 0-.117-.311v-.117a4.4 4.4 0 0 0-.116-.621v-.117a.5.5 0 0 0-.078-.35v-1.01q.1-.067.078-.195a.8.8 0 0 1 .039-.35q.063-.181.077-.388.095-.178.117-.389v-.039a.7.7 0 0 0 .117-.31 5.98 5.98 0 0 1 1.865-2.76.3.3 0 0 0 .195-.155 5.3 5.3 0 0 1 1.36-.855.6.6 0 0 0 .272-.117q.183-.104.389-.175.152-.048.31-.058.21-.024.389-.117h.039a6 6 0 0 0 .7-.116h.077a.5.5 0 0 0 .272-.04"
clipRule="evenodd"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M23.469 30.91q.1.066.077.194l-.078 12.553q.01-3.478-.019-6.956l-6.743-.078h6.762zM16.084 36.662q-.34.076-.7.116a.13.13 0 0 1 .079-.058 3.5 3.5 0 0 1 .621-.058M15.346 36.778a1.1 1.1 0 0 1-.389.117.73.73 0 0 1 .39-.117M14.258 37.128a.6.6 0 0 1-.272.117.37.37 0 0 1 .272-.117M12.626 38.1a.3.3 0 0 1-.195.155.3.3 0 0 1 .195-.155M10.566 41.015a.7.7 0 0 1-.117.31.5.5 0 0 1 .117-.31M10.45 41.364q-.024.21-.117.389a.73.73 0 0 1 .116-.389M10.333 41.753q-.015.207-.078.389a1.3 1.3 0 0 1 .02-.311.13.13 0 0 1 .057-.078M10.216 42.491q.021.128-.078.195-.023-.128.078-.195M10.138 43.696a.5.5 0 0 1 .078.35.44.44 0 0 1-.078-.35M10.216 44.163q.079.3.116.621a.13.13 0 0 1-.058-.077 4 4 0 0 1-.058-.544M23.391 44.435q0 .255-.117.466.03-.247.117-.466M10.333 44.9a.5.5 0 0 1 .116.312.7.7 0 0 1-.117-.311M10.838 46.222a.6.6 0 0 1 .078.156q-.098-.04-.078-.156M12.392 48.282q.15.053.233.194a.43.43 0 0 1-.233-.194M14.024 49.331a.3.3 0 0 1 .117.078q-.097.013-.117-.078M14.607 49.565a.23.23 0 0 1 .195.077q-.128.022-.195-.077M19 49.565a.52.52 0 0 1-.35.116q.16-.084.35-.116M14.996 49.681q.21.024.389.117a.73.73 0 0 1-.389-.117M18.65 49.681a.73.73 0 0 1-.39.117q.18-.093.39-.117M18.028 49.837q-.162.067-.35.077a.09.09 0 0 1 .04-.058q.154-.03.31-.02M15.424 49.798q.4.032.777.116a2 2 0 0 1-.777-.116M16.24 49.915q.195-.01.388.019a.1.1 0 0 1 .04.058.64.64 0 0 1-.428-.078M17.406 49.914a.09.09 0 0 1-.039.059.7.7 0 0 1-.233.02.34.34 0 0 1 .272-.079"
clipRule="evenodd"
/>
</svg>
)
}
export function GithubLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 139 62" {...props}>
<path
fill="currentColor"
d="M82.549 39.079h-.04c.02 0 .02.02.04.02h.02zm0 .02c-.158 0-.572.079-1.006.079-1.36 0-1.834-.631-1.834-1.46v-5.482h2.78c.159 0 .277-.138.277-.335v-2.978c0-.157-.138-.295-.276-.295h-2.78V24.92c0-.138-.1-.217-.257-.217h-3.786c-.158 0-.237.099-.237.217v3.806s-1.913.473-2.03.493a.296.296 0 0 0-.218.296V31.9c0 .197.138.335.296.335h1.933v5.758c0 4.28 2.977 4.713 5.008 4.713.927 0 2.051-.295 2.229-.394.098-.04.157-.158.157-.276v-2.623c0-.158-.098-.276-.256-.315m41.53-3.846c0-3.175-1.282-3.589-2.623-3.45-1.045.058-1.893.591-1.893.591v6.172s.848.592 2.13.631c1.794.04 2.386-.611 2.386-3.944m4.259-.276c0 6.015-1.952 7.73-5.344 7.73-2.879 0-4.417-1.459-4.417-1.459s-.059.809-.158.907c-.059.099-.138.138-.256.138h-2.583c-.178 0-.336-.138-.336-.295l.04-19.464c0-.158.138-.296.296-.296h3.727c.157 0 .296.138.296.296v6.606s1.439-.927 3.549-.927l-.02-.04c2.091 0 5.206.79 5.206 6.804m-15.282-6.33h-3.688c-.197 0-.296.138-.296.335v9.525s-.966.67-2.287.67c-1.302 0-1.696-.591-1.696-1.912v-8.322a.303.303 0 0 0-.296-.296h-3.747a.303.303 0 0 0-.296.296v8.973c0 3.845 2.15 4.811 5.108 4.811 2.445 0 4.417-1.34 4.417-1.34s.099.69.138.788c.04.079.158.158.276.158h2.347c.197 0 .296-.138.296-.296l.039-13.094c.02-.158-.138-.296-.315-.296m-41.53-.02h-3.727c-.158 0-.296.158-.296.355V41.84c0 .355.237.473.532.473h3.372c.355 0 .434-.158.434-.473V28.923c-.02-.157-.158-.295-.315-.295m-1.854-5.935a2.41 2.41 0 0 0-2.425 2.425 2.41 2.41 0 0 0 2.425 2.426c1.321 0 2.386-1.065 2.386-2.426 0-1.34-1.065-2.425-2.386-2.425m28.91-.434h-3.708a.303.303 0 0 0-.296.296v7.158h-5.797v-7.158a.303.303 0 0 0-.296-.296h-3.727a.303.303 0 0 0-.296.296v19.463c0 .158.158.296.296.296h3.727a.303.303 0 0 0 .296-.296v-8.321h5.797l-.04 8.321c0 .158.139.296.297.296h3.727a.303.303 0 0 0 .295-.296V22.554c.02-.158-.118-.296-.276-.296m-33.07 8.618v10.057c0 .059-.02.197-.1.217 0 0-2.188 1.557-5.797 1.557-4.358 0-9.544-1.36-9.544-10.372s4.515-10.866 8.933-10.846c3.825 0 5.363.868 5.6 1.025.06.099.099.158.099.237l-.71 3.135c0 .158-.158.355-.355.296-.631-.197-1.578-.572-3.806-.572-2.583 0-5.344.73-5.344 6.547 0 5.798 2.623 6.488 4.516 6.488 1.617 0 2.189-.197 2.189-.197v-4.023h-2.584c-.197 0-.335-.138-.335-.296v-3.234c0-.157.138-.296.335-.296h6.567c.197-.02.335.119.335.276"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M25.027 16.5C16.723 16.5 10 23.224 10 31.526c0 6.646 4.299 12.266 10.274 14.258.75.118 1.045-.316 1.045-.71 0-.355-.02-1.538-.02-2.8-3.766.69-4.752-.927-5.048-1.755-.177-.434-.907-1.775-1.538-2.13-.532-.276-1.282-.986-.02-1.006 1.184-.02 2.031 1.085 2.307 1.538 1.361 2.268 3.51 1.637 4.378 1.243.138-.967.533-1.637.967-2.012-3.333-.374-6.843-1.676-6.843-7.414 0-1.637.591-2.998 1.538-4.043-.158-.375-.67-1.913.158-3.983 0 0 1.262-.395 4.14 1.538a13.966 13.966 0 0 1 7.494 0c2.88-1.952 4.122-1.538 4.122-1.538.828 2.07.296 3.608.158 3.983.966 1.045 1.538 2.386 1.538 4.043 0 5.758-3.51 7.04-6.863 7.414.552.474 1.026 1.361 1.026 2.78 0 2.012-.02 3.63-.02 4.122 0 .395.276.868 1.025.71a15.065 15.065 0 0 0 10.235-14.257c0-8.283-6.724-15.007-15.026-15.007"
clipRule="evenodd"
/>
</svg>
)
}
export function GoodtapeLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 168 60" {...props}>
<path
fill="currentColor"
d="M44.925 43.193H12.81a2.645 2.645 0 0 1-2.652-2.635V22.442a2.645 2.645 0 0 1 2.652-2.635h32.114a2.645 2.645 0 0 1 2.652 2.635v18.116a2.645 2.645 0 0 1-2.652 2.635m-7.246-16.099c-2.482 0-4.495 2-4.495 4.466s2.014 4.466 4.495 4.466 4.495-2 4.495-4.466c0-2.465-2.014-4.466-4.495-4.466m-17.188 0c-2.48 0-4.494 2-4.494 4.466s2.013 4.466 4.494 4.466c2.482 0 4.495-2 4.495-4.466 0-2.465-2.013-4.466-4.495-4.466"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M108.451 25.946h-3.268l-.046 3.781a5 5 0 0 0-.269-.36l-.006-.007-.008-.01-.008-.008-.053-.055c-.736-.653-1.515-.974-2.511-.98l-.177.002c-.427.003-.84.008-1.256.113l-.083.025-.064.02a7 7 0 0 0-.491.163c-.694.3-1.446.796-1.762 1.51l-.035.101h-.093c-.447 1.016-.594 1.941-.567 3.044.05 1.08.456 2.107 1.241 2.852l.018.017.019.017.001.002.037.035a1.5 1.5 0 0 0 .412.304l.032.01.067.022v.093c1.233.557 2.61.87 3.943.421.664-.267 1.37-.656 1.67-1.346l.036-.102h.093c0 .054 0 .097.002.14.005.183.034.365.185 1.305v.002h2.941zm-3.696 5.493c.38.419.498.928.483 1.485-.028.4-.112.715-.335 1.052l-.084.128c-.399.41-.975.528-1.529.541-.645.008-1.21-.066-1.701-.529-.33-.33-.417-.797-.429-1.246.008-.589.143-1.13.569-1.555.821-.645 2.26-.576 3.026.124"
clipRule="evenodd"
/>
<path
fill="currentColor"
d="M66.966 26.215h.102c.51.003 1.012.013 1.514.105l.12.021c1.104.206 2.098.587 2.915 1.38l.03.026.058.051c.583.531.983 1.479 1.079 2.256v.374h-3.735l-.047-.158v-.002L69 30.266l-.01-.03-.04-.133-.025-.085-.017-.057h-.093q-.03-.052-.062-.105c-.204-.334-.584-.482-.948-.584-.783-.164-1.754-.142-2.447.298l-.091.064-.095.063-.002.002c-.437.33-.626.867-.71 1.391a5.9 5.9 0 0 0 .2 2.466h.093l.047.12c.235.403.631.612 1.067.747.315.082.628.079.951.076h.209c.728-.005 1.384-.069 1.928-.607.178-.196.273-.367.373-.616l-2.848-.047-.002-.954-.002-.302v-.359q0-.15.004-.3c.064-.062.141-.058.225-.054l.065.002h5.91c.061.004.086.005.106.016a.2.2 0 0 1 .044.037q.006.111.006.223v.479l-.001.231v1.023l-.001.35v.255l-.002.755v.387l-.002 2.039h-2.942v-.001c-.073-.59-.096-.774-.112-.959l-.01-.134-.005-.071-.008-.126-.007-.087-.008-.124-.01-.15-.009-.122-.005-.072-.006-.087-.008-.114-.022-.33-.023-.33-.027.131-.002.012-.022.106-.012.058-.024.112v.006l-.012.054c-.19.807-.626 1.477-1.319 1.939-1.193.712-2.772.736-4.094.405q-1.718-.47-2.611-1.983l-.06-.1c-.827-1.426-.778-3.56-.379-5.114.311-1.119 1.03-2.156 2.026-2.77h.094v-.093c.569-.294 1.112-.529 1.736-.683l.023-.006.026-.006.033-.008.034-.009c.63-.148 1.256-.192 1.9-.186"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M141.643 28.315h.018l.088-.001.088-.002c.405-.003.771.045 1.161.155l.111.03c1.01.285 1.782.972 2.289 1.875.231.462.355.95.448 1.456l.024.13c.184 1.241-.01 2.554-.76 3.577q-.12.156-.244.308l-.103.14c-.161.192-.373.339-.579.483h-.002l-.068.048-.042.03-.1.07c-.968.643-2.262.74-3.383.569a3.2 3.2 0 0 1-.672-.22l-.138-.06c-.575-.265-.897-.584-1.263-1.106l-.046 4.248h-3.315V28.514h2.895c.157 1.064.182 1.23.186 1.396v.098h.094a1 1 0 0 1 .034-.103c.092-.204.246-.36.401-.518l.078-.08.095-.1c.689-.671 1.683-.888 2.612-.891zm.842 4.027c-.097-.463-.276-.8-.654-1.08-.831-.515-2.219-.524-2.944.185-.358.402-.465.927-.469 1.456.021.487.114.91.472 1.26.611.51 1.34.52 2.101.466.491-.087.869-.256 1.19-.644.326-.466.372-1.092.304-1.643M79.26 28.266q.176 0 .351-.003c1.37-.005 2.7.35 3.77 1.231l.102.078c.772.624 1.243 1.658 1.346 2.63.061 1.387-.127 2.57-1.074 3.641l-.024.028-.032.038-.02.023c-.44.484-1.048.773-1.652.997q-.095.035-.188.072c-1.706.663-4.038.465-5.697-.238-.628-.291-1.25-.769-1.584-1.386v-.094h-.093c-.556-1.35-.78-2.86-.204-4.247.2-.442.441-.806.764-1.168l.089-.101a3.8 3.8 0 0 1 1.171-.833h.094v-.093c.963-.35 1.853-.578 2.882-.576m.112 2.652.142-.001c.543.01 1.08.137 1.478.528.426.52.49 1.23.429 1.877a1.8 1.8 0 0 1-.187.467l-.082.149c-.33.413-.708.6-1.225.691-.726.048-1.47.072-2.054-.42-.366-.333-.495-.773-.525-1.257-.015-.579.065-1.06.478-1.497.443-.428.951-.534 1.547-.537M91.516 28.263q-.176.003-.35.002c-.552 0-1.09.018-1.629.154l-.15.045q-.156.045-.31.097c-1.217.428-2.265 1.132-2.847 2.318-.504 1.106-.554 2.52-.147 3.662.417 1.071 1.057 1.801 2.107 2.283 1.585.684 3.932.808 5.553.177l.189-.072c.63-.233 1.23-.537 1.692-1.037l.045-.052.037-.043c.596-.68.988-1.53 1.035-2.44.044-1.347-.17-2.552-1.118-3.574-1.102-1.084-2.588-1.525-4.107-1.52m-.222 2.663h.037l.07-.002c.522 0 .995.112 1.41.437.11.117.196.24.28.374q.025.03.048.062l.035.046c.24.518.227 1.258.053 1.794a1.58 1.58 0 0 1-.782.823c-.383.142-.727.18-1.133.181l-.139.003c-.532.003-1.068-.121-1.475-.481-.265-.282-.414-.69-.434-1.074l-.007-.123c-.017-.533.06-1.057.427-1.465.227-.211.456-.362.747-.466l.053-.023.003-.001.013-.005q.022-.01.047-.02c.227-.061.446-.06.68-.06h.067M152.333 28.266q.19 0 .38-.003c1.33-.005 2.682.271 3.73 1.138l.099.076c.796.642 1.185 1.698 1.348 2.678v1.494h-7.656c.212.58.357.807.887 1.074.46.192.923.196 1.416.198l.179.003c.499.002.949-.064 1.346-.388l.011-.02.01-.018.009-.016.023-.042a.4.4 0 0 1 .087-.077.5.5 0 0 1 .156-.036l.194.001h.108q.177 0 .353.003h.244l.643.005.653.004h.003q.645.003 1.288.01l-.065.165-.068.172-.012.03-.026.067c-.132.333-.312.589-.53.873l-.067.094c-.785 1.013-2.222 1.373-3.423 1.527l-.104.013-.085.01-.026.003-.041.005c-1.739.17-3.75.047-5.176-1.066-.776-.675-1.153-1.597-1.302-2.594-.072-1.31.105-2.645 1.007-3.662l.125-.13.082-.083.004-.004.001-.001.022-.025.048-.055c.54-.595 1.361-.956 2.119-1.175l.058-.018.059-.018c.625-.184 1.242-.214 1.889-.213m.171 2.378.147-.002c.497.003 1.071.08 1.454.426.219.234.424.522.424.854h-4.248c.091-.457.369-.727.747-.98.441-.29.959-.294 1.471-.298zM128.843 28.269h-.132c-.415 0-.83 0-1.243.044l-.034.005-.033.004-.042.005c-1.076.128-2.087.487-2.941 1.167-.634.592-.904 1.299-.981 2.148h3.408l.001-.004c.112-.335.19-.57.486-.772.686-.371 1.734-.376 2.478-.173l.102.035.053.018c.156.051.304.1.396.245.044.188.058.375-.015.557a1.14 1.14 0 0 1-.443.232l-.07.011-.114.019c-.408.066-.819.11-1.23.15q-.366.036-.759.062c-1.037.074-2.168.154-3.038.643l-.042.024-.046.026c-.302.152-.587.306-.834.54-.441.504-.614 1.082-.625 1.748.022.603.213 1.148.657 1.573l.095.069.054.038c.223.173.449.302.712.406 1.58.526 3.243.26 4.703-.452v-.093h.093c.383-.268.672-.582.945-.958l.062-.087.021-.03.1.43.04.178.186.98h2.895v-.005l.002-2.001.001-1.126.001-.9v-.475l.001-.455v-.168c.003-.939-.172-1.807-.843-2.503-1.079-1.004-2.618-1.154-4.027-1.155m1.214 5.988c.281-.35.411-.672.383-1.121-.26 0-.612.12-.967.243-.296.102-.594.204-.84.237q-.105.014-.212.027c-.537.067-1.083.135-1.532.455-.163.15-.181.303-.194.519a.5.5 0 0 0 .021.127c.022.058.07.106.202.238.304.241.304.241 1.142.219a3.03 3.03 0 0 0 1.997-.944"
clipRule="evenodd"
/>
<path fill="currentColor" d="M113.4 26.46h10.084v2.987h-3.361v7.61h-3.455v-7.61H113.4z" />
</svg>
)
}
export function GopuffLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 95 60" {...props}>
<g clipPath="url(#a)">
<path
fill="currentColor"
d="M61.88 23.943v7.021c0 1.817-.595 2.898-1.994 2.898-1.391 0-1.994-1.08-1.994-2.898v-7.021h-3.968v7.288c0 3.817 2.383 5.393 5.956 5.393 2.95 0 5.955-1.576 5.955-5.393v-7.288zm-30.204-.28c-4.095 0-6.812 2.572-6.812 6.572 0 4.012 2.877 6.39 6.577 6.39 3.413 0 6.725-2.072 6.725-6.605 0-3.74-2.59-6.357-6.49-6.357m-.12 10.219c-1.694 0-2.604-1.602-2.604-3.732 0-1.843.723-3.732 2.616-3.732 1.794 0 2.516 1.889 2.516 3.719.007 2.24-.977 3.745-2.529 3.745M22.04 23.07c-1.332.886-1.68 2.24-1.68 2.24-.722-.885-2.214-1.589-3.96-1.589-2.918 0-5.97 2.534-5.97 6.533 0 3.55 2.577 6.279 5.97 6.279 1.404 0 2.355-.339 3.11-1.068v.267c0 2.254-1.431 3.263-3.312 3.263-1.84 0-3.138-.593-3.947-1.042l-.783 2.93c1.057.535 3.151 1.056 4.83 1.056 1.881 0 3.775-.352 5.16-1.518 1.445-1.237 1.94-3.198 1.94-5.575v-5.49c.027-1.903.723-3.238 1.58-4.182 0-.007-2.938-2.104-2.938-2.104M17.129 33.92c-1.693 0-2.604-1.602-2.604-3.732 0-1.843.723-3.732 2.617-3.732 1.78.013 2.502 1.889 2.502 3.719 0 2.24-.97 3.745-2.515 3.745m28.92-10.258c-4.25 0-6.565 2.742-6.565 6.8v11.203h3.962V35.53c.709.716 1.84 1.114 3.352 1.114 2.837 0 5.801-2.124 5.801-6.644 0-3.973-2.69-6.337-6.55-6.337m-.014 10.219c-1.693 0-2.603-1.602-2.603-3.732 0-1.843.723-3.732 2.617-3.732 1.793 0 2.516 1.889 2.516 3.719-.02 2.24-.984 3.745-2.53 3.745m38.382-12.584v-2.957c-.549-.156-1.231-.267-2.094-.267-1.158 0-2.53.326-3.527 1.238-1.13 1.029-1.606 2.677-1.606 4.149v.462h-4.095v-.625c-.06-2.996 2.951-2.124 3.299-2.013v-2.957c-.555-.156-1.238-.267-2.101-.267-1.158 0-2.53.326-3.526 1.238-1.131 1.029-1.606 2.67-1.606 4.149v.462h-1.693v2.846h1.693v9.588c0 2.631-1.88 2.254-2.717 2.124l-.422 2.846a5 5 0 0 0 1.693.312c1.794.014 3.065-.52 3.921-1.335 1.071-1.029 1.48-2.507 1.48-3.973l.026-9.555h4.062v9.588c.06 2.774-2.168 2.208-2.717 2.123l-.421 2.846a5 5 0 0 0 1.692.313c1.794.013 3.065-.521 3.922-1.335 1.07-1.03 1.472-2.508 1.472-3.973l.026-9.556h3.252v-2.826h-3.299v-.632c.04-3.029 2.938-2.11 3.286-2.013"
/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M10.43 18.061h74v23.878h-74z" />
</clipPath>
</defs>
</svg>
)
}
export function GumloopLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 188 60" {...props}>
<path
fill="currentColor"
d="m66.859 37.304.322 4.353c-.766.242-1.693.403-2.901.403-2.5 0-4.072-1.169-4.636-3.386-1.088 2.016-2.982 3.386-5.844 3.386-4.474 0-6.973-3.225-6.973-7.255V22.109h5.562v11.406c0 2.297 1.17 3.749 3.184 3.749 2.257 0 3.83-1.653 3.83-4.112V22.11h5.521v14.187c0 .766.403 1.21.968 1.21.322 0 .523-.04.967-.202m26.596-15.397c4.393 0 6.731 3.104 6.731 7.497v12.454h-5.562V30.613c0-2.499-1.25-3.91-3.265-3.91-2.217 0-3.748 1.653-3.748 4.112v11.043h-5.522V30.613c0-2.458-1.21-3.91-3.265-3.91-2.216 0-3.748 1.653-3.748 4.112v11.043h-5.522v-19.75h5.32l.162 2.862c1.128-1.813 3.063-3.063 5.803-3.063 3.185 0 5.28 1.653 6.207 4.111 1.008-2.378 3.144-4.11 6.409-4.11m17.571 15.397.323 4.353c-.766.242-1.693.403-2.902.403-3.225 0-4.877-1.935-4.877-5.441V13.242h5.522v23.054c0 .766.402 1.21.967 1.21.322 0 .524-.04.967-.202m11.624-15.397c6.126 0 10.157 4.071 10.157 9.996S128.776 42.06 122.65 42.06c-6.046 0-10.117-4.03-10.117-9.956 0-5.965 4.071-10.197 10.117-10.197m0 4.555c-2.741 0-4.554 2.217-4.554 5.562 0 3.265 1.813 5.481 4.554 5.481 2.821 0 4.554-2.216 4.554-5.481 0-3.346-1.733-5.562-4.554-5.562m21.895-4.555c6.127 0 10.157 4.071 10.157 9.996s-4.03 10.157-10.157 10.157c-6.045 0-10.116-4.03-10.116-9.956 0-5.965 4.071-10.197 10.116-10.197m0 4.555c-2.74 0-4.554 2.217-4.554 5.562 0 3.265 1.814 5.481 4.554 5.481 2.822 0 4.555-2.216 4.555-5.481 0-3.346-1.733-5.562-4.555-5.562m24.476-4.555c4.997 0 8.584 3.507 8.584 9.754 0 6.127-3.426 10.399-8.786 10.399-2.902 0-4.837-1.21-5.925-3.023V50h-5.522V22.068h5.361l.121 2.902c1.128-1.773 3.184-3.063 6.167-3.063m-1.452 15.518c2.62 0 4.434-2.177 4.434-5.522 0-3.265-1.693-5.36-4.393-5.36-3.063 0-4.716 2.579-4.716 4.796v1.25c0 2.377 1.733 4.836 4.675 4.836M21.364 11.038c.754-.32 2.168-.708 3.195-.868.772-.132 1.63-.16 3.91-.17h17.026c-.43 5.188-5.156 5.222-6.03 5.313s-7.65 0-7.65 0c-6.05.076-6.842.17-8.68.944-2.61 1.104-4.579 2.973-5.823 5.52-.744 1.52-.914 2.228-1.008 4.323-.141 2.756.716 5.162 2.648 7.503.999 1.208 3.034 2.595 4.617 3.142 1.329.463 1.678.52 3.251.576 1.357.047 1.677.028 2.526-.15 1.083-.227 2.073-.596 3.175-1.18 1.932-1.03 3.6-2.822 4.58-4.917.923-1.982 1.263-4.72.697-5.616a2.2 2.2 0 0 0-.5-.547c-.621-.453-.697-.453-5.823-.406l-4.815.057-.396.264c-.216.142-.518.462-.678.698-.358.576-.358 1.048-.01 1.737.208.425.34.557.764.764l.518.265h7.406v.226c0 .255-.47 1.321-.838 1.897-1.48 2.331-4.297 3.68-6.992 3.35-1.819-.217-3.27-.915-4.495-2.16-1.15-1.162-1.819-2.474-2.054-4.021-.405-2.614.612-5.295 2.591-6.87.98-.765 1.659-1.124 2.752-1.445l.885-.264 5.183.029c3.016.009 5.437.056 5.795.122.792.123 1.847.434 2.262.67.848.472 1.197.69 1.413.887.132.123.255.218.293.218.103 0 .716.613.933.943.122.18.254.35.282.378.283.255 1.037 2.142 1.197 2.972.254 1.36-.019 4.408-.547 6.06-1.036 3.265-3.213 6.398-5.738 8.276-3.044 2.255-6.314 3.36-9.942 3.36-2.78 0-5.07-.53-7.397-1.7-3.995-2.01-6.945-5.407-8.274-9.531-.442-1.387-.537-1.784-.725-3.114-.48-3.445.273-7.23 2.064-10.287a17 17 0 0 1 5.946-6.002c.951-.566 1.14-.66 1.743-.925.179-.075.528-.217.763-.32"
/>
</svg>
)
}
export function HappyteamsLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 160 60" {...props}>
<path
fill="currentColor"
d="M57.053 37.301v-5.4h-4.307v5.4h-2.31V25.224h2.31v4.842h4.307v-4.841h2.328V37.3zM68.972 34.854q0 .542.158.804.175.244.543.385l-.473 1.485q-.787-.07-1.296-.384-.49-.315-.752-.962a3 3 0 0 1-1.19 1.05 3.6 3.6 0 0 1-1.577.331q-1.382 0-2.188-.786-.805-.787-.805-2.063 0-1.503 1.085-2.29 1.103-.803 3.134-.803h1.155v-.524q0-.891-.42-1.241-.402-.367-1.313-.367-1.016 0-2.45.49l-.526-1.521q1.734-.664 3.361-.664 3.555 0 3.554 3.18zm-3.991 1.084q1.138 0 1.785-1.136v-1.87h-.927q-2.171 0-2.171 1.608 0 .663.332 1.03.333.368.98.368M76.48 27.794q1.751 0 2.592 1.275.84 1.276.84 3.583 0 1.434-.438 2.552-.42 1.1-1.26 1.73-.823.63-1.96.63-1.471 0-2.33-1.014v4.316l-2.24.245V28.056h1.96l.123 1.136a3.6 3.6 0 0 1 1.243-1.049q.7-.35 1.47-.35m-.857 8.056q1.943 0 1.943-3.18 0-1.695-.438-2.447-.437-.752-1.313-.752-.56 0-1.05.35a2.9 2.9 0 0 0-.84.909v4.16q.648.96 1.698.96M86.91 27.794q1.75 0 2.59 1.275.84 1.276.84 3.583 0 1.434-.437 2.552-.42 1.1-1.26 1.73-.824.63-1.961.63-1.47 0-2.329-1.014v4.316l-2.24.245V28.056h1.96l.123 1.136a3.6 3.6 0 0 1 1.243-1.049q.7-.35 1.47-.35m-.858 8.056q1.943 0 1.943-3.18 0-1.695-.438-2.447t-1.313-.752q-.56 0-1.05.35a2.9 2.9 0 0 0-.84.909v4.16q.647.96 1.698.96M96.92 37.336q-.56 1.713-1.698 2.656-1.137.962-3.116 1.119l-.228-1.625q.911-.141 1.453-.402.56-.245.893-.665t.613-1.118h-.77l-2.941-9.245h2.38l2.014 7.76 2.1-7.76h2.311zM109.137 25.225l-.245 1.834h-3.151v10.242h-2.328V27.059h-3.257v-1.835zM117.218 32.46q0 .175-.053.909h-5.812q.105 1.346.701 1.922.595.577 1.575.577.613 0 1.156-.192.542-.21 1.155-.63l.928 1.276q-1.54 1.242-3.431 1.241-2.12 0-3.274-1.293-1.138-1.294-1.138-3.53 0-1.416.49-2.535.49-1.135 1.418-1.765.928-.646 2.206-.646 1.943 0 3.011 1.24 1.068 1.224 1.068 3.426m-2.223-.647q0-2.43-1.803-2.429-.824 0-1.296.612-.455.594-.543 1.922h3.642zM125.902 34.854q0 .542.158.804.175.244.543.385l-.473 1.485q-.788-.07-1.296-.384-.49-.315-.752-.962a3 3 0 0 1-1.191 1.05 3.6 3.6 0 0 1-1.575.331q-1.383 0-2.189-.786-.805-.787-.805-2.063 0-1.503 1.085-2.29 1.103-.803 3.134-.803h1.155v-.524q0-.891-.42-1.241-.402-.367-1.313-.367-1.016 0-2.451.49l-.525-1.521q1.734-.664 3.361-.664 3.555 0 3.554 3.18zm-3.991 1.084q1.138 0 1.785-1.136v-1.87h-.927q-2.172 0-2.171 1.608 0 .663.332 1.03.333.368.981.368M138.435 27.794q1.12 0 1.786.768.665.753.665 2.045v6.694h-2.241v-6.344q0-1.486-1.015-1.486-.543 0-.946.35-.385.331-.805 1.048v6.432h-2.241v-6.344q0-1.486-1.015-1.486a1.42 1.42 0 0 0-.963.35q-.385.331-.805 1.048v6.432h-2.241v-9.245h1.943l.175 1.206q1.05-1.469 2.679-1.468.805 0 1.383.419.595.402.858 1.136.56-.752 1.225-1.154.683-.401 1.558-.401M146.411 27.794q1.874 0 3.291 1.1l-.875 1.311q-1.191-.75-2.328-.751-.666 0-1.033.262a.76.76 0 0 0-.368.664q0 .315.158.524t.56.402q.42.175 1.225.402 1.506.402 2.224 1.067.735.663.735 1.852 0 1.38-1.12 2.167-1.12.77-2.784.77a6 6 0 0 1-2.083-.35 5 5 0 0 1-1.611-.962l1.121-1.258q1.207.909 2.521.909.752 0 1.19-.297.455-.297.455-.822 0-.401-.175-.629-.157-.244-.595-.42-.42-.191-1.331-.454-1.417-.384-2.083-1.066-.665-.7-.665-1.73 0-.77.438-1.38.454-.613 1.26-.962.823-.35 1.873-.35M41.153 33.963c.184-.96.281-1.95.281-2.963 0-8.642-7.037-15.648-15.717-15.648S10 22.358 10 31s7.037 15.648 15.717 15.648c1.628 0 3.198-.246 4.674-.703a9 9 0 0 1-.548-2.611 13.1 13.1 0 0 1-4.126.662c-7.209 0-13.053-5.818-13.053-12.996s5.844-12.996 13.053-12.996S38.77 23.822 38.77 31c0 .927-.097 1.83-.282 2.703q.222-.01.45-.011c.763 0 1.506.094 2.215.27"
/>
<path
fill="currentColor"
d="M39.024 34.364c-4.708 0-8.524 3.8-8.524 8.487 0 1.015.253 2.089.507 2.89 1.313-1.434 10.009-11.14 10.009-11.14s-1.378-.237-1.992-.237"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M20.052 29.863c.38-.039.72.237.76.616.238 2.31 2.296 4.16 4.852 4.16 2.553 0 4.61-1.845 4.852-4.15a.692.692 0 0 1 1.378.143c-.321 3.05-3.01 5.386-6.23 5.386-3.223 0-5.915-2.343-6.23-5.399a.69.69 0 0 1 .618-.756M29.283 25.96c-.66 0-1.195.548-1.195 1.222 0 .675.535 1.222 1.195 1.222s1.194-.547 1.194-1.222-.534-1.221-1.194-1.221M22.01 25.96c-.659 0-1.195.548-1.195 1.222 0 .675.536 1.222 1.195 1.222.66 0 1.195-.547 1.195-1.222s-.535-1.221-1.194-1.221"
clipRule="evenodd"
/>
</svg>
)
}
export function HumataLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 122 60" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M13.643 25.095a.83.83 0 0 0-.832.831v9.148c0 .459.373.831.832.831h2.91V38.4h-2.91a3.326 3.326 0 0 1-3.326-3.326v-9.148a3.326 3.326 0 0 1 3.326-3.326h2.91v2.495zM30.274 35.905c.46 0 .832-.372.832-.831v-9.148a.83.83 0 0 0-.832-.831h-2.91V22.6h2.91a3.326 3.326 0 0 1 3.327 3.326v9.148a3.326 3.326 0 0 1-3.327 3.326h-2.91v-2.495z"
clipRule="evenodd"
/>
<path
fill="currentColor"
d="M29.859 30.463a2.015 2.015 0 0 0-1.729-1.995 5 5 0 0 1-4.135-4.14 2.03 2.03 0 0 0-2-1.728h-.073a2.015 2.015 0 0 0-1.995 1.728 5 5 0 0 1-4.14 4.135 2.03 2.03 0 0 0-1.728 2v.074c0 1.002.735 1.852 1.728 1.995a5 5 0 0 1 4.135 4.14c.148.99.997 1.726 2 1.728h.074a2.015 2.015 0 0 0 1.994-1.728 5 5 0 0 1 4.14-4.135 2.03 2.03 0 0 0 1.729-2zm-2.963.067c0 .02-.027.037-.054.042a5.835 5.835 0 0 0-4.82 4.816c0 .027-.019.05-.034.05h-.062c-.02 0-.037-.028-.041-.055a5.83 5.83 0 0 0-4.817-4.819c-.027 0-.05-.02-.05-.034v-.062c0-.02.028-.037.055-.042a5.835 5.835 0 0 0 4.819-4.816c0-.028.02-.05.034-.05h.062c.02 0 .037.027.042.054a5.83 5.83 0 0 0 4.817 4.82c.027 0 .049.02.049.034zM51.726 22.6h2.717v15.561h-2.717v-6.805h-7.301v6.805h-2.717V22.6h2.717v6.329h7.301zM65.627 26.783v11.379H63.08v-1.214c-.637.758-1.592 1.452-3.035 1.452-2.335 0-3.778-1.625-3.778-4.075v-7.542h2.547v6.979c0 1.387.594 2.276 1.974 2.276 1.146 0 2.292-.867 2.292-2.493v-6.762zM69.98 31.486v6.676h-2.547V26.783h2.547v1.214c.616-.759 1.57-1.43 3.057-1.43 1.337 0 2.334.607 2.95 1.582.679-.737 1.74-1.582 3.46-1.582 2.292 0 3.777 1.647 3.777 4.096v7.499h-2.547v-6.74c0-1.41-.509-2.428-1.867-2.428-1.083 0-2.208.867-2.208 2.492v6.676h-2.547v-6.74c0-1.41-.509-2.428-1.867-2.428-1.083 0-2.208.867-2.208 2.492M88.131 38.357c-2.122 0-3.757-1.322-3.757-3.36 0-2.167 1.592-3.12 3.736-3.576l3.077-.65v-.195c0-1.084-.552-1.756-1.91-1.756-1.21 0-1.846.564-2.144 1.67l-2.398-.564c.552-1.886 2.186-3.36 4.648-3.36 2.674 0 4.288 1.3 4.288 3.923v4.899c0 .65.275.845.976.758v2.016c-1.847.217-2.823-.152-3.205-1.084-.7.802-1.868 1.279-3.311 1.279m3.056-4.205v-1.387l-2.398.52c-1.082.239-1.889.585-1.889 1.604 0 .889.637 1.387 1.613 1.387 1.358 0 2.674-.737 2.674-2.124M96.053 35.3v-6.307h-1.55v-2.21h1.55v-3.338h2.505v3.338h2.334v2.21h-2.334v5.896c0 1.018.573 1.127 1.485 1.127a8 8 0 0 0 1.04-.065v2.189c-.466.086-1.103.151-1.761.151-2.123 0-3.269-.671-3.269-2.99M105.304 38.357c-2.122 0-3.756-1.322-3.756-3.36 0-2.167 1.592-3.12 3.735-3.576l3.077-.65v-.195c0-1.084-.551-1.756-1.909-1.756-1.21 0-1.847.564-2.144 1.67l-2.398-.564c.551-1.886 2.186-3.36 4.647-3.36 2.675 0 4.288 1.3 4.288 3.923v4.899c0 .65.277.845.977.758v2.016c-1.847.217-2.824-.152-3.206-1.084-.7.802-1.867 1.279-3.311 1.279m3.056-4.205v-1.387l-2.397.52c-1.082.239-1.889.585-1.889 1.604 0 .889.637 1.387 1.612 1.387 1.359 0 2.674-.737 2.674-2.124"
/>
</svg>
)
}
export function KikiLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 106 60" {...props}>
<path
fill="currentColor"
d="M10.637 50V12.764h10.11v15.054h.508l11.2-15.054h11.782L31.655 29.346 44.528 50H32.455l-8.363-13.964-3.346 4.364V50zM45.933 50V22.073h10.036V50zm5.018-30.836q-2.037 0-3.491-1.346t-1.455-3.236q0-1.89 1.455-3.237Q48.915 10 50.95 10q2.055 0 3.492 1.345 1.454 1.345 1.454 3.237 0 1.891-1.454 3.236-1.436 1.346-3.491 1.346M66.9 43.382l.074-11.927h1.309l6.763-9.382H86.32L74.974 36.69h-2.837zM57.884 50V12.764h10.036V50zm17.236 0-6.4-11.054 6.546-7.128L86.61 50z"
/>
<path
fill="currentColor"
d="M85.733 50V22.073h10.036V50zm5.018-30.836q-2.036 0-3.491-1.346t-1.455-3.236q0-1.89 1.455-3.237Q88.715 10 90.75 10q2.055 0 3.492 1.345 1.455 1.345 1.454 3.237 0 1.891-1.454 3.236-1.437 1.346-3.491 1.346"
/>
</svg>
)
}
export function LangchainLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 164 60" {...props}>
<g fill="currentColor" clipPath="url(#a)">
<path d="M47.027 24.422c-.634 0-1.23.245-1.676.691L43.55 26.91a2.36 2.36 0 0 0-.673 1.963c.064.53.297 1.007.673 1.381.26.259.546.423.894.537q.027.157.028.316c0 .477-.186.924-.522 1.259l-.11.11a3.9 3.9 0 0 1-1.558-.957c-.602-.6-1-1.358-1.15-2.19l-.02-.116-.092.074q-.093.075-.175.158l-1.802 1.797a2.363 2.363 0 0 0 1.676 4.034c.608 0 1.213-.23 1.675-.691l1.802-1.797a2.363 2.363 0 0 0 0-3.343 2.34 2.34 0 0 0-.872-.55 1.942 1.942 0 0 1 .552-1.705 4.1 4.1 0 0 1 1.59.99c.6.6.997 1.356 1.15 2.19l.02.116.092-.074q.092-.075.177-.16l1.801-1.796a2.362 2.362 0 0 0-1.676-4.034z" />
<path d="M46.061 17.85H22.484C15.766 17.85 10.3 23.3 10.3 30s5.466 12.15 12.183 12.15h23.578c6.718 0 12.184-5.45 12.184-12.15S52.779 17.85 46.06 17.85M34.431 36.9c-.388.08-.825.094-1.122-.216-.11.252-.366.12-.563.087l-.052.146c-.656.044-1.148-.624-1.462-1.128-.62-.336-1.326-.54-1.961-.89-.037.578.09 1.294-.462 1.667-.028 1.112 1.686.132 1.843.96-.121.014-.257-.02-.354.074-.447.434-.96-.328-1.475-.013-.692.347-.762.632-1.618.703-.048-.071-.028-.119.012-.162.241-.28.258-.609.67-.728-.424-.067-.779.166-1.136.351-.464.19-.46-.428-1.174.033-.08-.065-.042-.123.004-.173.181-.22.42-.254.69-.241-1.325-.737-1.949.9-2.56.086-.185.048-.254.214-.37.33-.1-.109-.024-.242-.02-.37-.119-.056-.27-.083-.234-.274-.234-.079-.398.06-.572.191-.157-.121.106-.299.155-.425.14-.243.46-.05.621-.225.46-.261 1.103.163 1.63.091.405.051.907-.364.703-.779-.433-.552-.357-1.276-.366-1.936-.054-.386-.981-.877-1.25-1.292-.33-.374-.589-.808-.847-1.235-.932-1.8-.639-4.111-1.813-5.782-.531.293-1.223.154-1.681-.238-.248.224-.259.519-.278.83-.593-.59-.52-1.709-.045-2.367.194-.26.425-.474.683-.663.059-.042.078-.083.077-.149.47-2.104 3.669-1.699 4.68-.208.733.918.955 2.132 1.787 2.983 1.12 1.223 2.395 2.3 3.428 3.6.978 1.19 1.676 2.585 2.285 3.99.248.467.25 1.04.619 1.438.181.24 1.068.896.876 1.128.11.24.94.572.652.805m15.54-7.18-1.802 1.798a4.15 4.15 0 0 1-1.717 1.031l-.033.01-.012.032a4.1 4.1 0 0 1-.947 1.46l-1.802 1.797a4.14 4.14 0 0 1-2.944 1.215 4.15 4.15 0 0 1-2.945-7.089l1.802-1.796a4.1 4.1 0 0 1 1.714-1.028l.033-.01.012-.032c.21-.551.529-1.045.95-1.465l1.801-1.797a4.14 4.14 0 0 1 2.945-1.215c1.113 0 2.159.431 2.945 1.215a4.12 4.12 0 0 1 1.218 2.937c0 1.11-.432 2.154-1.218 2.936z" />
<path d="M27.102 34.724c-.158.615-.21 1.661-1.011 1.692-.066.355.246.488.531.374.282-.129.416.102.51.332.436.063 1.079-.145 1.103-.659-.65-.373-.85-1.083-1.132-1.74zM69.532 24.219h-2.14v12.983h9.18v-2.068h-7.04zM87.331 37.202h2.14v-.023h.029l.004-.096c0-.027.008-.287-.031-.688v-4.832c0-1.818 1.325-2.645 2.557-2.645 1.325 0 1.97.714 1.97 2.183v6.1h2.14v-6.384c0-2.426-1.54-3.933-4.02-3.933-1.054 0-1.994.3-2.733.868l-.02-.69h-2.033v10.14zM104.851 27.807c-.744-.604-1.7-.923-2.779-.923-2.886 0-4.68 1.99-4.68 5.194s1.794 5.212 4.68 5.212c1.018 0 1.927-.277 2.642-.804-.061 1.572-1.046 2.505-2.659 2.505-1.358 0-2.125-.432-2.283-1.284l-.02-.106-2.08.635.02.085c.35 1.693 1.934 2.703 4.238 2.703 1.563 0 2.789-.425 3.645-1.265.863-.847 1.3-2.067 1.3-3.626v-9.07h-1.98l-.043.744zm-.134 4.36c0 1.935-.942 3.09-2.521 3.09-1.693 0-2.663-1.158-2.663-3.178s.971-3.161 2.663-3.161c1.541 0 2.506 1.15 2.521 3zM117.421 33.085c-.444 1.435-1.583 2.226-3.207 2.226-2.322 0-3.766-1.763-3.766-4.601s1.457-4.601 3.802-4.601c1.623 0 2.558.636 3.033 2.061l.222.667 2.021-.949-.189-.534c-.77-2.168-2.547-3.314-5.14-3.314-1.762 0-3.258.637-4.326 1.84-1.057 1.19-1.616 2.86-1.616 4.83 0 4.052 2.338 6.67 5.959 6.67 2.551 0 4.527-1.334 5.286-3.57l.193-.572-2.076-.79-.198.638zM125.414 26.884c-1.022 0-1.932.28-2.646.813v-4.723h-2.14v14.229h2.14v-5.639c0-1.83 1.325-2.663 2.558-2.663 1.324 0 1.969.715 1.969 2.184v6.119h2.141V30.8c0-2.378-1.578-3.914-4.021-3.914zM141.946 22.668c-.795 0-1.372.578-1.372 1.373 0 .794.577 1.372 1.372 1.372s1.372-.578 1.372-1.372-.577-1.373-1.372-1.373M149.568 26.884c-1.054 0-1.994.3-2.732.868l-.02-.69h-2.033v10.14h2.141v-5.639c0-1.818 1.325-2.645 2.557-2.645 1.325 0 1.969.714 1.969 2.183v6.1h2.141v-6.384c0-2.426-1.541-3.933-4.021-3.933zM142.986 27.062h-2.13v5.027a6 6 0 0 0-2.073-1.117v-.512c0-2.239-1.47-3.576-3.933-3.576-2 0-3.497.94-4.106 2.577l-.164.441 1.715 1.265.295-.767c.388-1.012 1.107-1.483 2.26-1.483 1.154 0 1.793.555 1.793 1.65v.056l-.208-.006c-2.291-.036-3.963.5-4.968 1.591-1.029 1.117-.939 2.37-.925 2.509l.009.096h.01c.16 1.562 1.524 2.565 3.507 2.565 1.092 0 2.101-.304 2.864-.86l.008.683h1.843v-3.172l-.04-.029c-.26-.19-.715-.437-1.398-.523a4 4 0 0 0-.608-.037h-.096v.292c0 .67-.77 1.614-2.485 1.614-1.266 0-1.455-.532-1.455-.85v-.032c.01-.142.07-.49.384-.815.4-.413 1.299-.896 3.315-.865 1.46.023 2.58.43 3.33 1.213.92.96 1.093 2.275 1.125 2.79v.415h2.129v-10.14zM81.797 26.84c-2 0-3.496.94-4.106 2.576l-.164.442 1.716 1.265.294-.767c.388-1.012 1.107-1.483 2.26-1.483 1.154 0 1.793.555 1.793 1.65v.256l-2.205.389c-2.584.457-3.894 1.562-3.894 3.282 0 1.721 1.383 2.884 3.523 2.884 1.093 0 2.101-.304 2.864-.86l.009.683h1.842v-6.74c0-2.24-1.47-3.577-3.932-3.577m1.793 5.945v.903c0 .671-.772 1.615-2.486 1.615-1.266 0-1.454-.532-1.454-.85 0-.282 0-.942 2.242-1.358z" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M10.3 17.76h143.291v24.48H10.3z" />
</clipPath>
</defs>
</svg>
)
}
export function LoopsLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 126 60" {...props}>
<path
fill="currentColor"
d="M23.783 18.931h-3.245A10.43 10.43 0 0 0 10.12 29.353a10.43 10.43 0 0 0 10.42 10.42h3.256a10.43 10.43 0 0 0 10.422-10.42 10.43 10.43 0 0 0-10.434-10.422M11.409 29.353a9.122 9.122 0 0 1 10.643-8.997 9.08 9.08 0 0 1 7.599 8.992 5.87 5.87 0 0 1-4.89 5.78 7.15 7.15 0 0 0 2.935-5.78 7.15 7.15 0 0 0-12.22-5.059 7.17 7.17 0 0 0-2.095 5.059 10.39 10.39 0 0 0 4.85 8.82 9.134 9.134 0 0 1-6.815-8.815zm10.747-5.629a5.85 5.85 0 0 1 4.233 5.629 5.85 5.85 0 0 1-4.233 5.626 5.85 5.85 0 0 1-4.233-5.626 5.85 5.85 0 0 1 4.233-5.629m1.627 14.746h-.037a9.4 9.4 0 0 1-1.48-.125 9.08 9.08 0 0 1-7.588-8.686v-.306a5.87 5.87 0 0 1 4.889-5.78 7.15 7.15 0 0 0-2.936 5.78 7.15 7.15 0 0 0 5.372 6.935 7.172 7.172 0 0 0 8.957-6.935 10.39 10.39 0 0 0-4.85-8.82 9.12 9.12 0 0 1 6.803 8.82 9.123 9.123 0 0 1-9.13 9.117M55.078 37.955h-14.84v-1.674c1.83-.308 2.19-.952 2.19-1.57V24.225c0-1.184-.36-1.674-2.19-1.803v-1.674h9.327v1.674c-1.83.129-2.19.62-2.19 1.803v11.094c0 .617.18 1.056 1.726 1.056h1.674c.952 0 2.086-.464 2.885-2.988h1.674zm1.338-6.312c0-4.171 3.016-7.263 7.291-7.263 4.51 0 7.11 3.04 7.11 6.748 0 4.15-3.064 7.19-7.263 7.19-4.586 0-7.138-2.964-7.138-6.675m9.56.955c0-4.096-1.418-6.544-2.809-6.544-1.314 0-1.854 1.65-1.854 4.019 0 4.174 1.442 6.57 2.806 6.57 1.292 0 1.856-1.624 1.856-4.045m6.44-.955c0-4.171 3.014-7.263 7.29-7.263 4.51 0 7.11 3.04 7.11 6.748 0 4.15-3.064 7.19-7.266 7.19-4.583 0-7.135-2.964-7.135-6.675m9.558.955c0-4.096-1.418-6.544-2.808-6.544-1.314 0-1.857 1.65-1.857 4.019 0 4.174 1.445 6.57 2.808 6.57 1.29 0 1.857-1.624 1.857-4.045m20.745-1.674c0 4.507-2.73 7.396-6.672 7.396a5.8 5.8 0 0 1-2.32-.439v1.906c0 .954.387 1.341 2.14 1.598v1.684h-8.425v-1.675c1.34-.308 1.495-.643 1.495-1.597V28.456c0-.671-.232-1.058-1.083-1.443l-.412-.18v-1.341l5.254-1.107h1.031v1.65a3.54 3.54 0 0 1 3.067-1.65c4.095-.005 5.925 3.15 5.925 6.538m-4.946.927c0-2.68-.74-4.998-2.525-4.998-.774 0-.902-.052-1.521.412v5.177c0 3.067.493 3.97 1.83 3.97 1.237 0 2.216-1.521 2.216-4.561m17.397-5.952-2.37 3.247c-1.134-2.268-2.219-3.22-3.503-3.22-.774 0-1.778.463-1.778 1.262 0 2.712 8.062.952 7.909 6.93-.078 2.163-2.164 4.192-5.771 4.192-3.144 0-5.77-1.573-6.411-2.14l2.784-2.91c1.546 2.422 2.808 3.504 4.043 3.504.644 0 1.778-.283 1.805-1.546 0-2.911-7.961-.567-7.936-6.41 0-2.756 2.293-4.438 5.951-4.438 2.88.01 4.737 1.014 5.277 1.529"
/>
</svg>
)
}
export function LovableLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 221 60" {...props}>
<path
fill="currentColor"
d="M33.095 49.978q-.675 0-1.357-.244a3.16 3.16 0 0 1-1.188-.748l-3.276-3.014q-6.624-6.12-11.63-11.818-5.007-5.699-5.007-12.117 0-5.128 3.444-8.571 3.443-3.444 8.533-3.444 2.901 0 5.652 1.338t4.828 4.164q2.265-2.825 4.904-4.164a12.2 12.2 0 0 1 5.577-1.338q5.09 0 8.534 3.444 3.443 3.444 3.443 8.57 0 6.42-4.997 12.128t-11.66 11.828l-3.255 2.994a3.16 3.16 0 0 1-1.188.748 4 4 0 0 1-1.358.244"
/>
<path
fill="currentColor"
d="M200.665 24.72c6.084 0 9.736 3.868 9.736 10.735v1.043h-14.996c.305 3.086 2.565 4.868 5.651 4.868 2.868 0 4.868-1.608 5.998-2.738l2.694 3.868c-1.781 1.652-4.476 3.607-8.692 3.607-6.563 0-10.823-4.259-10.823-10.648 0-6.39 4.129-10.735 10.432-10.735m-.261 4.302c-2.565 0-4.433 1.26-4.868 4.043h9.475c-.174-2.565-1.999-4.043-4.607-4.043M181.275 15.03h5.346v30.424h-5.346zM168.063 46.106c-3.26 0-5.433-1.39-6.693-3.477h-.087v2.825h-5.346V15.03h5.346V28.2h.087c1.26-2.087 3.433-3.478 6.693-3.478 5.259 0 9.649 3.825 9.649 10.692s-4.39 10.692-9.649 10.692m-1.348-5.042c3.217 0 5.434-2.26 5.434-5.65s-2.217-5.65-5.434-5.65-5.432 2.26-5.432 5.65 2.216 5.65 5.432 5.65M139.153 46.104c-5.259 0-9.649-3.825-9.649-10.692 0-6.868 4.39-10.692 9.649-10.692 3.26 0 5.433 1.39 6.694 3.477h.087V25.37h5.302v20.08h-5.302v-2.825h-.087c-1.261 2.087-3.434 3.477-6.694 3.477m1.348-5.042c3.216 0 5.433-2.26 5.433-5.65s-2.217-5.65-5.433-5.65c-3.217 0-5.433 2.26-5.433 5.65s2.216 5.65 5.433 5.65M118.604 39.282l4.737-13.908h6.085l-8.215 20.08h-5.215l-8.171-20.08h6.041zM96.647 24.72c6.389 0 11.127 4.39 11.127 10.692s-4.738 10.691-11.127 10.691-11.127-4.39-11.127-10.691c0-6.303 4.738-10.692 11.127-10.692m0 5.041c-3.26 0-5.65 2.217-5.65 5.65s2.39 5.65 5.65 5.65 5.607-2.216 5.607-5.65c0-3.433-2.348-5.65-5.607-5.65M76.562 15.03h5.346v30.424H76.56z"
/>
</svg>
)
}
export function MarkpromptLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 177 60" {...props}>
<path
fill="currentColor"
d="M47.366 22.504a1.293 1.293 0 0 1 1.292-1.292h2.585a1.29 1.29 0 0 1 1.292 1.292v6.462a1.293 1.293 0 0 1-1.292 1.293h-2.585a1.293 1.293 0 0 1-1.292-1.293zm5.686 13.183a3.102 3.102 0 1 1-6.204 0 3.102 3.102 0 0 1 6.204 0M21.775 38.79a1.29 1.29 0 0 1-1.292-1.293V22.504a1.29 1.29 0 0 1 1.292-1.292h3.256c.393 0 .764.178 1.01.485l3.772 4.715a1.292 1.292 0 0 0 2.019 0l3.772-4.715a1.3 1.3 0 0 1 1.01-.485h3.255a1.293 1.293 0 0 1 1.293 1.292v14.992a1.293 1.293 0 0 1-1.293 1.293h-2.584a1.294 1.294 0 0 1-1.293-1.293v-5.104c0-1.221-1.538-1.761-2.301-.807l-1.86 2.324a1.292 1.292 0 0 1-2.018 0l-1.859-2.324c-.763-.954-2.301-.414-2.301.807v5.104a1.294 1.294 0 0 1-1.293 1.293zM72.638 24.01h2.173l3.778 9.224h.139l3.778-9.224h2.172v11.866h-1.703V27.29h-.11l-3.5 8.57h-1.414l-3.5-8.576h-.11v8.593h-1.703zM89.732 36.073a3.6 3.6 0 0 1-1.53-.313 2.6 2.6 0 0 1-1.083-.921q-.394-.603-.394-1.477 0-.754.29-1.24.29-.488.782-.771t1.1-.429q.61-.145 1.24-.22l1.299-.15q.498-.064.724-.203.225-.14.226-.452v-.04q0-.76-.429-1.177-.423-.417-1.263-.417-.875 0-1.379.388-.498.383-.69.852l-1.628-.371q.29-.811.846-1.31.563-.504 1.292-.73a5 5 0 0 1 1.536-.231q.533 0 1.13.127a3.3 3.3 0 0 1 1.124.452q.527.33.863.944.336.609.336 1.582v5.91h-1.692V34.66h-.07q-.168.336-.503.66a2.7 2.7 0 0 1-.864.54q-.527.214-1.263.214m.377-1.39q.718 0 1.228-.284.516-.285.782-.742.273-.465.273-.99v-1.148q-.093.093-.36.174-.26.075-.596.133-.337.053-.655.098-.319.041-.533.07a4.3 4.3 0 0 0-.921.214q-.412.15-.661.435-.243.278-.243.742 0 .642.475.973.475.325 1.21.325M96.432 35.876v-8.9h1.674v1.414h.093a2.15 2.15 0 0 1 .857-1.13q.62-.417 1.402-.417a7 7 0 0 1 .736.04v1.658a3 3 0 0 0-.371-.064 3.5 3.5 0 0 0-.533-.04q-.614 0-1.095.26a1.94 1.94 0 0 0-1.031 1.744v5.435zM104.287 32.857l-.012-2.114h.301l3.546-3.767h2.075l-4.045 4.288h-.272zm-1.594 3.02V24.01h1.733v11.866zm5.621 0-3.187-4.23 1.193-1.211 4.12 5.44zM111.776 39.213V26.976h1.691v1.443h.145q.15-.278.435-.643.284-.365.788-.637.504-.279 1.332-.278 1.078 0 1.924.544.846.545 1.327 1.57.487 1.026.487 2.469t-.481 2.474q-.481 1.024-1.321 1.581-.841.55-1.918.55-.811 0-1.327-.271a2.5 2.5 0 0 1-.8-.638 5 5 0 0 1-.446-.649h-.104v4.722zm1.697-7.787q0 .939.272 1.646.273.707.788 1.106.516.394 1.264.394.776 0 1.297-.411.522-.417.788-1.13.273-.712.273-1.605 0-.88-.267-1.581-.26-.702-.788-1.107-.521-.405-1.303-.406-.754 0-1.275.388-.516.389-.782 1.084-.267.695-.267 1.622M121.846 35.876v-8.9h1.674v1.414h.093q.243-.718.857-1.13a2.46 2.46 0 0 1 1.402-.417 7 7 0 0 1 .736.04v1.658a3 3 0 0 0-.371-.064 3.5 3.5 0 0 0-.533-.04q-.614 0-1.095.26a1.94 1.94 0 0 0-1.031 1.744v5.435zM131.537 36.056q-1.251 0-2.184-.574a3.86 3.86 0 0 1-1.448-1.605q-.516-1.03-.516-2.41 0-1.384.516-2.422a3.85 3.85 0 0 1 1.448-1.61q.933-.574 2.184-.574t2.185.573q.933.575 1.448 1.61.516 1.038.516 2.423 0 1.38-.516 2.41a3.86 3.86 0 0 1-1.448 1.605q-.933.574-2.185.574m.006-1.455q.811 0 1.344-.428a2.5 2.5 0 0 0 .788-1.142q.261-.712.261-1.57 0-.852-.261-1.564a2.54 2.54 0 0 0-.788-1.153q-.532-.435-1.344-.435-.816 0-1.356.435-.532.435-.793 1.153-.255.712-.255 1.564 0 .858.255 1.57.26.713.793 1.142.54.428 1.356.428M137.62 35.876v-8.9h1.663v1.449h.11q.278-.735.909-1.147.631-.417 1.513-.417.892 0 1.494.417.609.417.899 1.147h.092a2.5 2.5 0 0 1 1.014-1.136q.696-.428 1.657-.428 1.212 0 1.976.759.771.759.771 2.288v5.968h-1.733v-5.805q0-.905-.492-1.31a1.8 1.8 0 0 0-1.176-.406q-.846 0-1.316.522-.47.516-.469 1.327v5.672h-1.727V29.96q0-.724-.452-1.164t-1.176-.44q-.492 0-.909.26a1.9 1.9 0 0 0-.667.713 2.2 2.2 0 0 0-.249 1.06v5.487zM152.04 39.213V26.976h1.691v1.443h.145q.152-.278.435-.643t.788-.637q.504-.279 1.333-.278 1.077 0 1.923.544.846.545 1.327 1.57.487 1.026.487 2.469t-.481 2.474q-.481 1.024-1.321 1.581-.84.55-1.918.55-.811 0-1.327-.271a2.5 2.5 0 0 1-.8-.638 5 5 0 0 1-.446-.649h-.104v4.722zm1.697-7.787q0 .939.273 1.646.272.707.788 1.106.514.394 1.263.394.776 0 1.298-.411.52-.417.788-1.13a4.5 4.5 0 0 0 .272-1.605q0-.88-.267-1.581-.26-.702-.788-1.107-.521-.405-1.303-.406-.753 0-1.275.388-.516.389-.782 1.084-.267.695-.267 1.622M166.264 26.976v1.391h-4.861v-1.39zm-3.558-2.132h1.733v8.419q0 .504.15.759.151.249.389.342.243.087.527.087.209 0 .365-.03.156-.027.243-.046l.313 1.431a3 3 0 0 1-.429.116 3.4 3.4 0 0 1-.695.07 3.05 3.05 0 0 1-1.275-.243 2.2 2.2 0 0 1-.956-.788q-.365-.534-.365-1.339z"
/>
</svg>
)
}
export function MdnLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 118 60" {...props}>
<path
fill="currentColor"
d="m21.001 16.004-7.727 24.884h-3.161l7.7-24.884zm2.81 0v24.884h-2.81V16.004zm10.889 0L27 40.888h-3.162l7.7-24.884zm2.81 0v24.884H34.7V16.004zM63.042 35.728h-4.62v-1.486h1.027v-4.269c0-1-.19-1.702-.568-2.134-.378-.433-.891-.622-1.54-.622-.864 0-1.513.297-1.945.919-.432.621-.649 1.243-.676 1.864v4.215h1.622V35.7h-4.62v-1.486h1.026v-4.242c0-1.027-.19-1.73-.567-2.134-.379-.406-.892-.622-1.513-.622-.865 0-1.486.297-1.946.892-.432.594-.648 1.215-.675 1.864v4.269h1.864v1.486h-5.458v-1.486h1.621v-6.836h-1.648v-1.513h3.62v1.73a4.2 4.2 0 0 1 1.27-1.406c.541-.35 1.19-.54 1.973-.54s1.432.19 2.026.567c.595.379 1 .946 1.216 1.73a3.95 3.95 0 0 1 1.324-1.648c.595-.433 1.297-.649 2.135-.649.945 0 1.756.297 2.404.892.649.594 1 1.513 1 2.756v4.917h1.648zm11.483 0h-3.567v-1.945a4.44 4.44 0 0 1-1.323 1.513c-.568.432-1.297.648-2.243.648-1.297 0-2.35-.432-3.134-1.324-.784-.864-1.162-2.08-1.162-3.593 0-1.351.351-2.594 1.08-3.702.703-1.108 1.865-1.675 3.46-1.675 1.593 0 2.647.594 3.323 1.756v-5.322h-2.324V20.57h4.269v13.672h1.621zm-3.594-4.54V30c-.026-.81-.324-1.486-.864-1.972a2.83 2.83 0 0 0-1.945-.757c-.973 0-1.703.351-2.216 1.054-.513.702-.757 1.567-.757 2.594 0 1.107.27 1.972.811 2.566.54.595 1.216.892 2 .892.864 0 1.567-.351 2.107-1.027.567-.702.838-1.432.865-2.161m11.402 4.54v-1.486h1.621v-4.269c0-1-.189-1.702-.594-2.134-.378-.433-.919-.622-1.54-.622-.838 0-1.513.27-2 .81a2.96 2.96 0 0 0-.783 1.892v4.296h1.621V35.7h-5.214v-1.486h1.62v-6.809H75.39v-1.513h3.674v1.675c.703-1.242 1.837-1.864 3.431-1.864.973 0 1.81.297 2.486.892.676.594 1 1.513 1 2.756v4.917h1.62v1.459zM89.088 41.618h18.103v2.378H89.088z"
/>
</svg>
)
}
export function MobbinLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 136 61" {...props}>
<path
fill="currentColor"
d="M46.046 29.768h-8.978v8.877h8.978zm-35.912 8.877V30.83l8.93-8.83h7.903v7.816l-8.93 8.83z"
/>
<path
fill="currentColor"
d="M23.6 38.646V30.83L32.53 22h7.904v7.816l-8.93 8.83zm41.143-4.952L60.15 23.252h-3.71v13.933h2.77l.007-9.39 4.191 9.39h2.672l4.198-9.387v9.387h2.771V23.252h-3.713zm14.999-6.947c-4.62 0-5.59 2.857-5.59 5.252 0 3.66 1.847 5.438 5.644 5.438s5.608-1.763 5.608-5.388-1.905-5.304-5.66-5.304zm-2.709 5.203c0-1.83.418-2.76 2.709-2.76 2.132 0 2.782.676 2.782 2.897S81.882 35 79.778 35s-2.746-.711-2.746-3.048zm15.644-5.186h-.054c-1.664.008-2.963 1.18-3.305 1.712v-5.224H86.51v13.933h2.808v-.094l.015-1.329c.456.736 1.677 1.678 3.274 1.678 3.49 0 4.98-1.567 4.98-5.236 0-3.67-1.514-5.438-4.91-5.438zM94.76 32c0 2.773-1.293 2.997-2.856 2.997s-2.82-.22-2.82-2.93c0-2.06.8-2.863 2.856-2.863s2.82.704 2.82 2.795zm10.105-5.235h-.054c-1.664.008-2.963 1.18-3.305 1.712v-5.224H98.7v13.933h2.807v-.094l.015-1.329c.456.734 1.676 1.675 3.274 1.675 3.49 0 4.98-1.566 4.98-5.236 0-3.668-1.514-5.437-4.91-5.437M106.95 32c0 2.773-1.293 2.997-2.856 2.997s-2.82-.22-2.82-2.93c0-2.06.801-2.863 2.857-2.863s2.819.704 2.819 2.795zm6.823-5.015h-2.808v10.201h2.808zm7.524-.223c-1.558 0-2.818.967-3.222 1.715v-1.492h-2.807v10.201h2.807V31.24c0-1.333.401-1.969 2.677-1.969 1.724 0 2.336.532 2.336 2.035v5.878h2.807v-5.27c0-3.834-1.16-5.135-4.598-5.156zm-7.524-3.509h-2.808v2.643h2.808z"
/>
</svg>
)
}
export function MozillaLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 114 60" {...props}>
<path
fill="currentColor"
d="M103.092 37.493a2 2 0 0 1-.545.089c-.608 0-.894-.26-.894-1.008v-5.65c0-2.966-2.364-4.411-5.16-4.411-2.136 0-3.284.26-5.565 1.18l-.51 2.993 2.967.317.421-1.466c.609-.317 1.211-.374 1.99-.374 2.105 0 2.136 1.585 2.136 2.91v.431c-.665-.088-1.413-.114-2.136-.114-2.967 0-6.053.748-6.053 3.95 0 2.706 2.13 3.72 4.006 3.72 2.105 0 3.43-1.268 4.178-2.593.171 1.585 1.122 2.593 2.853 2.593.805 0 1.642-.229 2.333-.608zm-7.981-.083c-1.123 0-1.528-.665-1.528-1.502 0-1.413 1.154-1.787 2.479-1.787.603 0 1.267.088 1.876.171-.089 2.167-1.502 3.118-2.827 3.118m-4.994-17.47-5.623 19.835h-3.663l5.623-19.835zm-7.55 0-5.623 19.835h-3.658l5.623-19.835zm-15.61 6.833h3.892v4.666h-3.892zm0 8.33h3.892v4.672h-3.892zm-5.664-.172 2.941.286-.805 4.558h-11.36l-.374-1.96 7.15-8.215h-4.063l-.577 2.016-2.681-.291.462-4.557h11.417l.29 1.959-7.212 8.22h4.21zm-18.629-8.417c-4.671 0-6.978 3.143-6.978 6.947 0 4.152 2.769 6.6 6.775 6.6 4.152 0 7.15-2.625 7.15-6.776 0-3.633-2.28-6.771-6.947-6.771m-.088 10.693c-2.016 0-3.056-1.73-3.056-3.98 0-2.452 1.18-3.86 3.087-3.86 1.756 0 3.17 1.179 3.17 3.803 0 2.479-1.268 4.037-3.201 4.037m-10.237-.26h1.73v2.828h-5.45v-7.322c0-2.25-.749-3.113-2.22-3.113-1.787 0-2.509 1.268-2.509 3.087v4.52h1.73v2.828h-5.445v-7.322c0-2.25-.749-3.113-2.22-3.113-1.787 0-2.51 1.268-2.51 3.087v4.52h2.48v2.828H10v-2.827h1.73v-7.353H10v-2.827h5.446v1.959c.78-1.382 2.136-2.219 3.95-2.219 1.875 0 3.6.894 4.24 2.796.722-1.73 2.192-2.796 4.24-2.796 2.332 0 4.468 1.414 4.468 4.495v5.945z"
/>
</svg>
)
}
export function NextdoorlendingLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 123 60" {...props}>
<path
fill="currentColor"
d="m19.807 31.797-5.87-8.745v8.745h-3.511V17.62h3.51l5.871 8.785V17.62h3.512v14.177zM36.596 26.992h-7.944q.082 1.049.688 1.606.606.555 1.49.555 1.313 0 1.827-1.091h3.736a5 5 0 0 1-1.036 1.998 5.2 5.2 0 0 1-1.879 1.393q-1.13.504-2.524.504-1.683 0-2.996-.707a5.1 5.1 0 0 1-2.053-2.02q-.74-1.314-.74-3.07 0-1.758.73-3.071a5 5 0 0 1 2.041-2.02q1.311-.7 3.015-.701 1.663.001 2.956.686a5 5 0 0 1 2.022 1.958q.73 1.272.73 2.969 0 .484-.063 1.011m-3.53-1.92q0-.89-.615-1.415-.616-.525-1.54-.525-.883 0-1.49.504-.605.504-.747 1.435zM45.279 31.797l-2.197-3.13-1.846 3.13H37.44l3.777-5.757-3.88-5.512h3.942l2.196 3.11 1.847-3.11h3.796l-3.84 5.674 3.943 5.595zM56.96 28.87v2.93h-1.787q-1.908 0-2.977-.918-1.067-.92-1.069-2.999v-4.487h-1.396v-2.868h1.396v-2.745h3.511v2.748h2.3v2.867h-2.3v4.525q0 .504.247.728c.166.15.437.222.822.222h1.252zM72.643 18.508q1.703.89 2.638 2.495t.933 3.705q0 2.082-.933 3.695-.934 1.617-2.65 2.505c-1.14.593-2.467.889-3.972.889h-5.398V17.62h5.398q2.28 0 3.984.888m-1.11 9.231q1.11-1.069 1.11-3.03t-1.11-3.05q-1.11-1.092-3.099-1.091H66.77v8.24h1.662q1.991 0 3.102-1.069M110.106 20.941a4 4 0 0 1 2.053-.536v3.655h-.965q-1.313 0-1.971.555t-.656 1.95v5.232h-3.512v-11.27h3.512v1.88a4.6 4.6 0 0 1 1.539-1.466M27.459 41.049h2.207v1.28h-3.89v-6.805h1.683zM37.082 36.852v1.377h2.256v1.28h-2.256v1.494h2.551v1.329h-4.235v-6.808h4.235v1.328zM51.84 42.33h-1.684l-2.817-4.197v4.196h-1.683v-6.805h1.683l2.818 4.215v-4.215h1.683zM62.488 35.95q.818.428 1.267 1.198.447.772.447 1.78 0 .999-.447 1.774a3.05 3.05 0 0 1-1.272 1.203q-.823.428-1.906.427h-2.593v-6.808h2.593q1.094.001 1.911.427m-.531 4.432q.532-.513.531-1.454 0-.94-.531-1.465-.533-.524-1.489-.523h-.797v3.954h.797q.958 0 1.489-.512M71.759 35.524v6.805h-1.684v-6.805zM84.093 42.33H82.41l-2.817-4.197v4.196h-1.684v-6.805h1.684l2.817 4.215v-4.215h1.684zM94.83 37.677a1.23 1.23 0 0 0-.536-.518 1.8 1.8 0 0 0-.822-.179q-.817 0-1.31.529-.492.527-.493 1.41 0 .941.518 1.468.517.528 1.423.528.623 0 1.05-.31.426-.308.626-.89H93.15v-1.222h3.666v1.542a3.2 3.2 0 0 1-.634 1.153 3.3 3.3 0 0 1-1.14.861q-.69.33-1.555.331-1.026 0-1.828-.44a3.16 3.16 0 0 1-1.25-1.227q-.448-.784-.448-1.793 0-1.01.448-1.798a3.16 3.16 0 0 1 1.247-1.23q.799-.44 1.822-.44 1.243 0 2.093.592.852.591 1.129 1.638H94.83zM88.817 23.348a5.3 5.3 0 0 0-2.153-2.046q-1.369-.713-3.064-.713-1.697 0-3.064.713a5.3 5.3 0 0 0-2.153 2.046q-.785 1.333-.786 3.087-.001 1.773.775 3.094a5.26 5.26 0 0 0 2.131 2.036q1.356.711 3.053.712 1.696 0 3.075-.712a5.36 5.36 0 0 0 2.174-2.047q.798-1.333.798-3.086 0-1.748-.786-3.084m-1.907 6.32h-2.329V27.22h-1.96v2.447h-2.33V25.78l3.309-2.09 3.308 2.09v3.887zm.689-4.322a.386.386 0 0 1-.531.112L83.6 23.241l-3.468 2.217a.386.386 0 0 1-.532-.112.37.37 0 0 1 .114-.523l3.677-2.35a.39.39 0 0 1 .42 0l3.677 2.35c.176.115.228.35.111.523M102.325 23.348a5.3 5.3 0 0 0-2.153-2.046q-1.366-.713-3.064-.713-1.696 0-3.064.713a5.3 5.3 0 0 0-2.153 2.046q-.785 1.333-.786 3.087 0 1.773.775 3.094a5.27 5.27 0 0 0 2.132 2.036q1.355.711 3.052.712 1.697 0 3.075-.712a5.37 5.37 0 0 0 2.176-2.047q.796-1.333.796-3.086 0-1.748-.786-3.084m-1.906 6.32h-2.332V27.22h-1.96v2.447h-2.33V25.78l3.309-2.09 3.307 2.09v3.887zm.688-4.322a.385.385 0 0 1-.531.112l-3.468-2.217-3.468 2.217a.386.386 0 0 1-.531-.112.37.37 0 0 1 .114-.523l3.677-2.35a.39.39 0 0 1 .42 0l3.676 2.35c.176.115.228.35.111.523"
/>
</svg>
)
}
export function PebblelyLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 151 60" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M36.426 17.632c-2.641-.657-5.146.496-6.867 2.196l-2.19 2.163a6.7 6.7 0 0 1-3.007 1.712l-7.913 2.075c-2.34.614-4.963 2.248-5.418 5.187-.377 2.436.1 5.268 2.5 7.533 2.344 2.212 6.33 3.695 12.547 3.986 9.823.458 16.505-1.293 19.845-4.84 1.717-1.822 2.485-4.058 2.35-6.495-.132-2.397-1.13-4.921-2.808-7.438-2.543-3.814-6.344-5.41-9.039-6.08m-4.985 4.101c1.287-1.27 2.879-1.866 4.34-1.503 2.349.584 5.426 1.921 7.456 4.966 1.508 2.263 2.265 4.33 2.362 6.1.096 1.732-.43 3.243-1.625 4.512-2.484 2.638-8.094 4.452-17.771 4-5.924-.276-9.157-1.676-10.834-3.258-1.62-1.528-1.966-3.401-1.691-5.175.215-1.392 1.55-2.508 3.45-3.006l7.914-2.075a9.4 9.4 0 0 0 4.208-2.398zM84.97 20.12v8.75a4.1 4.1 0 0 1 1.216-1.261q.994-.64 2.253-.64 1.326 0 2.453.64 1.126.618 1.811 1.812.685 1.17.685 2.85 0 1.656-.685 2.85-.684 1.192-1.811 1.833a5 5 0 0 1-2.453.619q-1.26 0-2.253-.64a4.1 4.1 0 0 1-1.215-1.261v1.68h-2.409V20.12zm.42 13.918q-.42-.729-.42-1.767 0-1.039.42-1.746.442-.729 1.105-1.104t1.348-.376q.84 0 1.524.376.707.375 1.127 1.104t.42 1.746-.42 1.745-1.127 1.105q-.684.375-1.524.375-.685 0-1.348-.375a3.1 3.1 0 0 1-1.105-1.083M58.858 21.887v15.465h2.607v-5.855h2.054q1.68 0 2.917-.574 1.26-.596 1.944-1.68.685-1.08.685-2.562 0-1.48-.685-2.54-.686-1.083-1.944-1.658-1.237-.597-2.917-.596zm2.607 2.297V29.2h2.054q1.348 0 2.166-.641.816-.64.817-1.878 0-1.238-.817-1.856-.818-.64-2.166-.64zM70.63 35.054a4.6 4.6 0 0 0 1.812 1.856q1.192.663 2.74.663 1.767 0 3.026-.685 1.26-.684 2.055-2.054l-2.055-.84q-.486.818-1.215 1.215t-1.701.398q-.928 0-1.59-.398-.642-.398-.973-1.149a4.2 4.2 0 0 1-.3-1.237h7.9q.022-.089.022-.287.022-.2.022-.376 0-1.635-.618-2.784a4.13 4.13 0 0 0-1.724-1.79q-1.126-.618-2.74-.618-1.612 0-2.805.663a4.54 4.54 0 0 0-1.856 1.834q-.64 1.192-.64 2.806 0 1.59.64 2.783m1.863-3.866h5.286a2.7 2.7 0 0 0-.322-1.149 2.24 2.24 0 0 0-.884-.928q-.552-.33-1.303-.331-.906 0-1.547.397a2.5 2.5 0 0 0-.972 1.15q-.17.384-.258.861M97.98 28.87v-8.75h-2.407v17.232h2.408v-1.68q.49.783 1.215 1.26.994.64 2.254.641 1.325 0 2.452-.619 1.127-.64 1.812-1.833.684-1.194.684-2.85 0-1.68-.684-2.85-.686-1.194-1.812-1.812a4.9 4.9 0 0 0-2.452-.64q-1.26 0-2.254.64a4.1 4.1 0 0 0-1.215 1.26m0 3.4q0 1.04.42 1.768.442.706 1.105 1.083.663.375 1.348.375.84 0 1.524-.375.707-.376 1.127-1.105.42-.73.42-1.745 0-1.017-.42-1.746a2.8 2.8 0 0 0-1.127-1.104 3.1 3.1 0 0 0-1.524-.376q-.685 0-1.348.376-.663.375-1.104 1.104-.42.707-.42 1.746"
clipRule="evenodd"
/>
<path fill="currentColor" d="M108.671 20.12h2.386v17.232h-2.386z" />
<path
fill="currentColor"
fillRule="evenodd"
d="M113.976 35.054a4.6 4.6 0 0 0 1.811 1.856q1.194.663 2.74.663 1.767 0 3.027-.685 1.259-.684 2.054-2.054l-2.054-.84q-.486.818-1.215 1.215t-1.702.398q-.927 0-1.59-.398a2.5 2.5 0 0 1-.972-1.149 4.2 4.2 0 0 1-.301-1.237h7.901q.022-.089.022-.287.022-.2.022-.376 0-1.635-.619-2.784a4.13 4.13 0 0 0-1.723-1.79q-1.126-.618-2.74-.618-1.613 0-2.805.663a4.54 4.54 0 0 0-1.856 1.834q-.64 1.192-.641 2.806 0 1.59.641 2.783m1.862-3.866h5.287a2.7 2.7 0 0 0-.322-1.149 2.24 2.24 0 0 0-.884-.928q-.552-.33-1.304-.331-.906 0-1.546.397-.64.399-.972 1.15a4 4 0 0 0-.259.861"
clipRule="evenodd"
/>
<path
fill="currentColor"
d="M125.996 20.12h2.387v17.232h-2.387zM133.797 42.213l6.629-15.024h-2.563l-2.635 6.606-2.668-6.606h-2.739l4.087 8.97-2.651 6.054z"
/>
</svg>
)
}
export function PikaLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 82 60" {...props}>
<path
fill="currentColor"
d="M33.9 20.487h-2.911v3.047h2.91zm37.293 15.506v-7.279c0-4.254-2.663-6.152-8.638-6.152-5.02 0-8.015 2.256-8.015 6.037h2.935c0-2.263 1.567-3.27 5.08-3.27 4.245 0 5.751.94 5.751 3.593-1.037.14-2.022.263-2.984.383-7.214.902-11.568 1.447-11.568 5.622 0 1.637.627 2.798 1.918 3.547 1.155.67 3.214 1.04 5.795 1.04 3.478 0 6.372-.846 7.203-2.103-.076 1.143 0 1.853 0 1.86h2.647c-.004-.013-.124-1.315-.124-3.278m-2.887-3.427c0 1.774-.629 2.992-1.924 3.72-1.157.65-2.897.954-5.47.954-1.578 0-4.223-.303-4.223-2.337 0-.992.69-1.598 2.381-2.088 1.823-.53 4.886-.94 9.238-1.426v1.177zm-11.47 2.302h-.073v.069h.072zM20.88 20.487H10.43V39.27h2.911v-6.683h7.54c7.041 0 7.802-4.231 7.802-6.049-.002-1.82-.763-6.051-7.803-6.051m-.525 9.312H13.34v-6.524h7.016c3.645 0 5.2.975 5.2 3.261C25.555 28.823 24 29.8 20.355 29.8m33.022-7.124h-4.032L39.3 30.465v-9.978h-2.91V39.27h2.91v-7.93l10.14 7.93h4.222l-10.83-8.573zM33.9 24.852h-2.911V39.27h2.91z"
/>
</svg>
)
}
export function PwcLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 113 60" {...props}>
<g clipPath="url(#a)">
<mask
id="b"
width="96"
height="56"
x="7"
y="18"
maskUnits="userSpaceOnUse"
style={{ maskType: 'luminance' as const }}
>
<path fill="#fff" d="M102.931 18.71H7.608v54.48h95.323z" />
</mask>
<g mask="url(#b)">
<path fill="currentColor" d="M102.931 42.104H76.766V35.42h26.165z" />
<path fill="currentColor" d="M99.532 42.104H76.766V25.39h22.766z" />
<path fill="currentColor" d="M90.126 42.103h-13.36V18.711h13.36z" />
<path fill="currentColor" d="M92.755 42.103h-15.99V21.321h15.99z" />
<path fill="currentColor" d="M76.762 21.325v20.776h13.363V21.325z" />
<path fill="currentColor" d="M63.368 42.104h13.398v3.34H63.368z" />
<path fill="currentColor" d="M90.046 25.386v10.081h2.717v-10.08z" />
<path fill="currentColor" d="M76.762 25.386v16.715h13.363V25.386z" />
<path fill="currentColor" d="M85.12 35.533h-8.354v-3.461h8.355z" />
<path fill="currentColor" d="M76.762 35.426V42.1H99.54v-6.675z" />
<path fill="currentColor" d="M90.11 35.426V42.1h2.653v-6.675z" />
<path fill="currentColor" d="M76.762 35.426V42.1h13.363v-6.675z" />
<path fill="currentColor" d="M76.762 35.426V42.1h8.367v-6.675z" />
<path
fill="currentColor"
d="M65.304 24.755c-2.28.404-3.495 2.123-3.495 5.105 0 3.033 1.62 5.055 4.002 5.055 1.165 0 2.178-.405 4.305-1.466v2.477c-2.583 1.213-4.103 1.516-6.13 1.516-2.279 0-3.9-.556-5.166-1.92-1.317-1.315-2.026-3.033-2.026-5.005 0-4.296 3.242-7.279 7.953-7.279 3.09 0 5.268 1.466 5.268 3.539 0 1.364-1.013 2.325-2.482 2.325-.76 0-1.368-.253-2.23-.708M53.906 30.517c2.077-2.578 2.837-3.64 2.837-4.903 0-1.264-1.014-2.376-2.432-2.376-.81 0-1.57.455-1.925.86v5.307l-3.394 4.55V23.642H45.75l-5.37 8.846v-8.846h-1.823l-4.914 1.112v1.264l2.685.252V37.24h3.394l5.167-8.492v8.492h3.8M23.36 35.521h.76c2.735 0 4.255-1.668 4.255-5.004 0-2.78-1.216-4.296-3.496-4.296-.304 0-.76.05-1.52.202zm0 5.965 2.229.506v1.263h-8.764v-1.264l2.026-.505V26.221h-2.229v-1.264l5.32-1.617h1.418v1.82c2.482-1.517 3.343-1.77 4.66-1.77 2.938 0 5.167 2.73 5.167 6.52 0 4.348-2.989 7.33-7.548 7.33-.506 0-1.368-.05-2.28-.101"
/>
</g>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M10.43 18.5h92v27h-92z" />
</clipPath>
</defs>
</svg>
)
}
export function QuivrLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 114 60" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M31.228 42.414c.029.132.127.153.226.174.067.014.134.028.179.077q.021.024.047.043l.014.012.012.01q-.012.007-.025.012-.027.01-.05.023a.38.38 0 0 1-.32.044q-.096-.023-.194-.045c-.25-.056-.499-.112-.721-.254-.012-.007-.031-.002-.05.003l-.03.006.001.004v.006l.001.005.001.009c.002.017.003.034.012.043.131.14.21.3.212.497 0 .064.008.126.015.19l.01.081c-.134.005-.137-.079-.14-.159-.002-.035-.003-.069-.015-.095-.127-.267-.351-.424-.576-.581q-.062-.044-.123-.088c-.076-.055-.166-.093-.255-.131l-.047-.02q-.152-.065-.306-.122-.053-.02-.103-.04.023.054.051.107c.04.078.081.157.09.238.02.187.018.376.016.565l-.001.163c0 .015-.013.03-.026.044l-.017.02-.015-.025a.2.2 0 0 1-.024-.047c-.069-.308-.165-.602-.426-.807a5 5 0 0 0-.411-.299.6.6 0 0 0-.153-.06q-.04-.012-.079-.026l.017.065q.02.079.047.156c.149.38.162.593.058.983q-.009.025-.027.047l-.008.011-.008.012q-.006-.013-.017-.025c-.012-.015-.023-.03-.024-.046-.02-.518-.324-.882-.628-1.247q-.06-.07-.117-.142-.062-.074-.128-.146l-.071-.079-.04-.05-.021-.028-.01.024a.2.2 0 0 0-.015.054c-.012.312-.006.618.117.92.073.178.12.367.165.557l.052.207c.004.015-.006.034-.016.053l-.012.024-.024-.011c-.017-.007-.034-.014-.042-.027l-.125-.19v-.002c-.16-.242-.318-.484-.465-.733a.7.7 0 0 1-.08-.321q-.009-.198-.007-.395.001-.168-.004-.336c-.001-.027-.026-.053-.051-.079l-.024-.026.001.236c.002.202.003.405-.009.605-.024.393-.07.785-.23 1.152a.4.4 0 0 1-.05.078l-.03.04q-.005-.02-.012-.037a.2.2 0 0 1-.02-.08q-.001-.118.002-.235a3 3 0 0 0-.007-.41l-.011-.116c-.047-.479-.093-.958-.323-1.397q-.016-.028-.04-.051l-.02-.023-.004.012-.006.012c-.008.018-.016.036-.014.054.026.242-.04.468-.105.693q-.047.15-.08.3l-.015.07c-.143.686-.15.722.016 1.493a.2.2 0 0 1-.005.086l-.006.038-.03-.026a.2.2 0 0 1-.053-.059c-.258-.528-.414-1.06-.263-1.667.049-.194.041-.402.033-.61a4 4 0 0 1-.004-.333 4.8 4.8 0 0 0-.177-1.552l-.032-.118-.01-.044-.016.057c-.014.044-.028.089-.03.134q-.002.11 0 .223.004.125 0 .25l-.019.419c-.015.386-.03.772-.076 1.155a2.38 2.38 0 0 1-.838 1.568q-.02.017-.048.025l-.02.009-.014-.041c-.01-.029-.021-.057-.012-.078q.055-.12.115-.24.063-.126.12-.253c.072-.166.142-.333.186-.507a.5.5 0 0 0-.005-.201q-.008-.045-.012-.09l-.055.081q-.046.066-.09.133l-.085.13q-.083.132-.17.26c-.154.224-.298.45-.27.74a.2.2 0 0 1-.004.085l-.079.229-.03.085-.022-.099c-.016-.068-.032-.136-.035-.204-.016-.384.011-.753.156-1.125.164-.42.218-.876.187-1.334q-.005-.041-.023-.08l-.014-.037-.015.034q-.017.036-.03.074l-.088.285c-.06.2-.12.399-.195.592-.178.46-.485.839-.791 1.217l-.07.088-.486.603c-.023-.27.085-.5.193-.73q.04-.082.075-.165.014-.033.022-.068l.004-.017.002-.008.003-.01-.036.003a.2.2 0 0 0-.079.017c-.407.254-.815.508-1.063.944-.01.017-.038.023-.066.029l-.04.01q.001-.025.005-.05l.002-.02.004-.035c.035-.353.205-.634.45-.883.187-.192.36-.399.528-.608l.004-.005c.083-.102.17-.211.157-.358l-.067.052c-.053.04-.106.079-.149.126-.504.557-1.154.838-1.864 1.021-.369.095-.733.206-1.098.317l-.351.107q-.114.036-.223.086l-.09.038c-.029.011-.052.035-.075.059-.045.047-.09.093-.176.045l.038-.057q.048-.075.097-.147c.271-.396.688-.569 1.118-.71.205-.068.415-.124.624-.18l.304-.083q.031-.008.063-.023l.014-.005-.025-.02a.2.2 0 0 0-.056-.035c-.54-.144-1.055-.145-1.53.215-.415.316-.903.416-1.414.492-.432.064-.863.061-1.293.058-.223-.001-.446-.003-.669.005-.027 0-.054-.01-.081-.02q-.02-.008-.037-.014l.01-.005.016-.009q.037-.021.076-.032l.341-.09h.001c.332-.086.664-.173.998-.248.68-.153 1.326-.386 1.876-.822.365-.29.785-.366 1.224-.39.675-.035 1.271-.24 1.738-.753.443-.486.589-1.04.41-1.684-.228-.817-.758-1.333-1.542-1.617-.33-.12-.658-.235-1.016-.252-.581-.028-1.008.217-1.32.697a8 8 0 0 1-.2.29c-.104.146-.208.292-.297.448-.209.367-.563.508-.923.626-.555.181-1.043-.03-1.51-.311a.5.5 0 0 1-.07-.055q-.031-.028-.065-.05c-.171-.112-.338-.225-.409-.43l.185.089q.17.08.338.165c.456.23.919.252 1.39.052.338-.143.577-.409.765-.708.1-.161.18-.372.17-.555-.012-.212-.084-.45-.353-.526q-.233-.068-.465-.144c-.312-.1-.624-.2-.943-.258-.448-.081-.813.183-1.136.469a6 6 0 0 0-.304.298q-.12.126-.246.246c-.39.362-.835.582-1.384.517a1 1 0 0 1-.235-.066l-.045-.016c-.019-.006-.034-.022-.048-.038l-.024-.023.023-.009q.027-.012.054-.017.078-.01.156-.016c.089-.008.177-.017.264-.035.608-.127 1.024-.512 1.34-1.023.111-.18.22-.362.311-.552.228-.476.858-.78 1.405-.54q.196.085.397.161.212.08.42.172c.158.071.314.115.488.117.161.002.205-.072.186-.209q-.016-.124-.036-.25-.027-.166-.046-.336c-.004-.032.011-.067.026-.1q.012-.026.02-.05.017.014.04.027c.031.02.063.04.069.065.108.474.482.649.885.749q.267.066.534.128h.002v.001h.002c.487.116.974.232 1.454.378.146.045.288.029.4-.073.1-.092.194-.189.118-.362l-.016-.035c-.05-.118-.099-.229-.19-.328-.504-.552-1.117-.959-1.754-1.334-.605-.358-1.236-.335-1.872-.08q-.251.103-.498.216h-.002l-.273.12c-.6.262-1.22.223-1.827.064a1.32 1.32 0 0 0-.977.107c-.2.1-.39.226-.577.35-.33.218-.652.428-1.047.534-.36.095-.646-.036-.942-.173l-.054-.024c-.01-.005-.015-.02-.02-.035l-.008-.02.025-.01a.14.14 0 0 1 .05-.017q.193-.001.387.002c.355.006.659-.111.93Line truncated
clipRule="evenodd"
/>
<path
fill="currentColor"
d="m37.399 30.786.008.002-.007.008zM22.106 43.542l.004-.012v-.001l.008-.01.001.015.002.001h.001l-.002.002v.002l-.002-.002zM22.106 43.542l-.006.016-.012.032-.028-.029.012-.005.02-.008zM38.243 29.84c.15-.011.366-.266.362-.427-.005-.164-.27-.422-.427-.414-.163.007-.42.277-.411.431.012.191.284.427.476.41M10.951 28.999c.185.004.425.265.423.46-.001.145-.256.39-.397.384-.244-.012-.39-.184-.38-.453.006-.213.172-.396.354-.391M13.91 22.882c.24.002.391-.15.389-.393-.002-.224-.153-.386-.352-.39a.43.43 0 0 0-.421.427c-.001.2.163.355.383.356M35.91 23.141c-.2-.003-.394-.197-.377-.375.017-.171.244-.407.387-.402.198.008.458.255.451.432-.007.18-.233.349-.462.345M23.755 18.094c.001-.244-.146-.39-.391-.39-.223 0-.385.149-.385.353-.001.205.186.418.374.427a.39.39 0 0 0 .402-.39M17.66 21.847c.197.008.401.235.38.422-.027.216-.182.353-.396.35-.208-.002-.353-.168-.35-.395s.16-.386.366-.377M15.097 40.509c.122-.014.317-.24.307-.354-.013-.142-.24-.332-.384-.324-.15.01-.298.19-.293.354.006.166.206.342.37.323M38.531 27.65c.003.196-.156.375-.32.362-.163-.014-.294-.171-.293-.354.001-.225.084-.309.303-.31.225-.003.308.078.31.301M34.417 40.51c.115-.007.33-.233.323-.343-.012-.188-.19-.351-.365-.335-.146.013-.311.207-.303.356.006.113.235.327.344.322M10.742 33.466c.154.01.302.186.296.353-.007.164-.181.323-.332.3a.326.326 0 0 1-.28-.32c.005-.17.168-.342.316-.333M38.793 34.143c.138-.006.259-.15.262-.312q.008-.345-.255-.36c-.111-.006-.335.194-.348.312-.014.133.206.365.341.36M28.377 24.593c.162-.008.34.159.336.31-.003.132-.172.29-.313.295-.186.007-.27-.081-.277-.293-.006-.21.07-.304.254-.312M12.629 36.73c.139-.006.303-.185.29-.316-.016-.141-.198-.3-.325-.281-.136.02-.292.216-.267.336.025.123.19.266.302.261M34.21 23.402c.094-.007.265.162.279.276.017.143-.118.318-.25.322-.14.005-.327-.134-.343-.252-.014-.1.202-.338.314-.346M13.007 25.861c-.01-.16-.11-.245-.28-.238-.142.007-.32.168-.31.284.005.087.222.306.3.303.155-.007.301-.183.29-.349M19.215 18.65c.165.01.252.11.244.28-.008.168-.106.256-.273.25-.143-.004-.338-.173-.323-.28.016-.115.216-.256.352-.25M30.892 19.604c-.006.093.195.323.286.325.087.003.294-.217.295-.312.001-.104-.17-.267-.284-.27-.107-.004-.29.155-.297.257M16.121 25.205c-.167.007-.267-.078-.276-.239-.012-.183.088-.291.27-.29q.251.004.255.26c0 .168-.085.261-.249.27M16.623 28.134q.254 0 .259-.258.003-.257-.247-.27c-.171-.007-.27.074-.282.236-.013.188.083.293.27.292M28.487 17.596c-.007.183-.1.295-.238.285-.141-.01-.3-.183-.288-.314.01-.108.18-.26.297-.267.117-.005.235.145.229.296M37.408 30.798l.007-.007.005.012zM37.42 30.803l.052.02-.014.015-.015.017-.014-.032zM77.956 26.023v11.774h3.265V26.023zM78.347 24.022q.52.483 1.25.483.728 0 1.24-.483.522-.49.522-1.173 0-.675-.521-1.157a1.73 1.73 0 0 0-1.242-.49q-.728 0-1.25.49a1.53 1.53 0 0 0-.513 1.157q0 .683.514 1.173"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M61.156 34.317a7 7 0 0 1-1.514 1.948l2.25 2.82h-3.097l-1.264-1.553q-1.27.48-2.753.48-2.093 0-3.756-.95t-2.63-2.752q-.965-1.802-.965-4.362 0-2.568.966-4.37.965-1.801 2.63-2.744 1.662-.95 3.755-.95 2.078 0 3.741.95 1.671.942 2.637 2.744.974 1.802.974 4.37t-.974 4.37m-7.528-1.978h2.821l1.162 1.495a4.3 4.3 0 0 0 .655-1.08q.498-1.143.498-2.806t-.498-2.806Q57.775 26 56.88 25.41q-.897-.59-2.1-.59t-2.101.59-1.395 1.732q-.49 1.143-.49 2.806t.49 2.805q.498 1.143 1.395 1.733t2.1.59q.453 0 .863-.084z"
clipRule="evenodd"
/>
<path
fill="currentColor"
d="M72.078 32.784v-6.761h3.266v11.774h-3.136V35.66h-.122a3.48 3.48 0 0 1-1.327 1.663q-.92.63-2.246.629-1.181 0-2.077-.537a3.7 3.7 0 0 1-1.403-1.525q-.497-.99-.506-2.37v-7.496h3.266v6.914q.007 1.043.56 1.649.55.605 1.479.605.59 0 1.104-.268.513-.276.828-.813.321-.537.314-1.326M94.814 26.023l-4.116 11.774h-3.68l-4.116-11.774h3.45l2.445 8.424h.123l2.437-8.424zM96.486 26.023v11.774h3.265v-6.661q0-.72.322-1.273.33-.552.889-.858.568-.314 1.288-.314.337 0 .744.053.407.054.636.123v-2.898a4.933 4.933 0 0 0-1.027-.115q-.989 0-1.748.567-.759.56-1.08 1.656h-.124v-2.054z"
/>
</svg>
)
}
export function ResendLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 100 60" {...props}>
<path
fill="currentColor"
d="M10.983 38.903V21.829h7.561q1.586 0 2.878.732a5.25 5.25 0 0 1 2.025 1.927q.756 1.22.756 2.78 0 1.538-.756 2.805a5.37 5.37 0 0 1-2.025 1.976q-1.291.732-2.878.732H14.52v6.122zm9.683 0-4.341-7.708 3.756-.61 4.83 8.342zm-6.146-9.098h3.853q.66 0 1.147-.293a2.1 2.1 0 0 0 .78-.854q.27-.56.269-1.244 0-.73-.317-1.268a2.1 2.1 0 0 0-.927-.83Q18.715 25 17.91 25h-3.39zM32 39.147q-2.097 0-3.683-.854a6.26 6.26 0 0 1-2.439-2.342q-.854-1.488-.854-3.414 0-1.512.488-2.757.487-1.243 1.366-2.146a5.9 5.9 0 0 1 2.073-1.415 6.7 6.7 0 0 1 2.635-.512q1.316 0 2.439.488 1.122.487 1.927 1.366.829.853 1.268 2.049.44 1.17.39 2.536l-.024 1.074H27.22l-.561-2.122h8.073l-.39.439V31a2.15 2.15 0 0 0-.44-1.17 2.47 2.47 0 0 0-.95-.83 2.7 2.7 0 0 0-1.318-.317q-1.073 0-1.829.415a2.73 2.73 0 0 0-1.122 1.219q-.39.805-.39 1.951 0 1.17.488 2.025.512.853 1.415 1.341.925.465 2.17.464.855 0 1.561-.269.708-.267 1.513-.926l1.658 2.317q-.707.633-1.56 1.073a8.7 8.7 0 0 1-1.757.634 7.5 7.5 0 0 1-1.78.22M43.292 39.147q-1.804 0-3.22-.586-1.389-.61-2.244-1.658L40 35.049a4.4 4.4 0 0 0 1.659 1.17 5.3 5.3 0 0 0 1.854.342q.366 0 .658-.073a2 2 0 0 0 .537-.244q.22-.171.317-.39a1.1 1.1 0 0 0 .122-.512.94.94 0 0 0-.44-.83q-.243-.146-.755-.317-.512-.195-1.317-.414-1.245-.318-2.123-.732-.853-.44-1.365-.976a4.1 4.1 0 0 1-.683-1.097 4 4 0 0 1-.22-1.342q0-.878.39-1.585.39-.732 1.074-1.244a5.3 5.3 0 0 1 1.61-.78 6.1 6.1 0 0 1 1.902-.293q1 0 1.951.243a7 7 0 0 1 1.756.708 6 6 0 0 1 1.44 1.049l-1.854 2.048a6 6 0 0 0-1-.756 5 5 0 0 0-1.122-.536 3.4 3.4 0 0 0-1.05-.195q-.414 0-.755.073-.317.073-.537.244-.22.146-.341.366a1.19 1.19 0 0 0 .024 1q.147.243.39.414.27.147.78.342.538.195 1.44.439 1.17.317 1.976.707.83.39 1.317.903.415.439.61 1t.195 1.244q0 1.194-.683 2.122-.66.926-1.83 1.463t-2.634.537M55.81 39.147q-2.097 0-3.682-.854a6.26 6.26 0 0 1-2.44-2.342q-.853-1.488-.853-3.415 0-1.511.488-2.756.487-1.243 1.366-2.146a5.9 5.9 0 0 1 2.073-1.415 6.7 6.7 0 0 1 2.634-.512 6.05 6.05 0 0 1 2.44.488q1.121.487 1.926 1.366.83.853 1.269 2.049.438 1.17.39 2.536l-.025 1.073H51.03l-.56-2.122h8.073l-.39.44V31a2.15 2.15 0 0 0-.44-1.17 2.47 2.47 0 0 0-.95-.83 2.7 2.7 0 0 0-1.318-.317q-1.073 0-1.83.414a2.73 2.73 0 0 0-1.121 1.22q-.39.805-.39 1.951 0 1.17.487 2.025.513.854 1.415 1.341.927.465 2.17.464.855 0 1.562-.269.706-.268 1.512-.927l1.659 2.318q-.708.633-1.561 1.073a8.7 8.7 0 0 1-1.757.634 7.5 7.5 0 0 1-1.78.22M62.76 38.903V26h3.317l.098 2.634-.683.293a3.9 3.9 0 0 1 .951-1.586 5.4 5.4 0 0 1 1.683-1.17 4.9 4.9 0 0 1 2.05-.44q1.463 0 2.438.586 1 .586 1.488 1.78.512 1.172.512 2.903v7.903h-3.463v-7.635q0-.878-.244-1.463-.245-.585-.756-.854-.488-.268-1.22-.244-.585 0-1.097.195-.489.171-.854.513a3 3 0 0 0-.561.78q-.195.44-.195.951v7.757H62.76M81.764 39.147q-1.708 0-3.05-.854-1.316-.878-2.097-2.366-.756-1.512-.756-3.488 0-1.926.756-3.439.78-1.536 2.098-2.39 1.341-.879 3.048-.878.904 0 1.732.292.854.27 1.512.756.684.488 1.098 1.098.415.585.464 1.22l-.879.17v-8.415h3.488v18.05h-3.292l-.147-2.976.683.073a2.36 2.36 0 0 1-.439 1.171 4.2 4.2 0 0 1-1.049 1.024q-.633.44-1.463.708a5.9 5.9 0 0 1-1.708.244m.78-2.878q.976 0 1.707-.488a3.3 3.3 0 0 0 1.147-1.342q.414-.854.414-2 0-1.121-.414-1.976a3.3 3.3 0 0 0-1.147-1.341q-.73-.488-1.707-.488-.976 0-1.707.488-.708.487-1.122 1.342-.39.853-.39 1.975 0 1.146.39 2 .414.855 1.122 1.342.731.488 1.707.488"
/>
</svg>
)
}
export function ShotgunLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 134 60" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="m20.34 30.42 4.124-5.95a1.62 1.62 0 0 0 0-1.86 2.16 2.16 0 0 0-1.792-.93h-9.905a2.15 2.15 0 0 0-1.758.913 1.64 1.64 0 0 0 0 1.86l10.548 15.331H13.9l2.687-3.888-1.521-2.231-4.057 5.865a1.64 1.64 0 0 0 0 1.86 2.15 2.15 0 0 0 1.758.93h9.905a2.15 2.15 0 0 0 1.758-.93 1.64 1.64 0 0 0 0-1.86L13.9 24.233h7.674l-2.756 3.955zm16.227-3.854c1.86 0 3.296.27 4.31.828s1.64 1.47 1.893 2.772h-2.687a1.64 1.64 0 0 0-1.133-1.285 8 8 0 0 0-2.417-.287c-1.014 0-1.69.118-2.08.321-.388.203-.574.507-.574.845 0 .203.034.338.102.474.084.135.236.236.473.32s.575.17 1.031.22c.456.051 1.065.102 1.809.136q1.577.078 3.127.389c.794.202 1.403.507 1.775.946.371.423.557.98.557 1.69 0 1.1-.44 1.961-1.318 2.57-.879.625-2.265.93-4.124.93-.71 0-1.353-.034-1.91-.102a8.3 8.3 0 0 1-1.539-.321 4.23 4.23 0 0 1-2.062-1.285 5.1 5.1 0 0 1-1.014-2.248h2.738c.17.71.592 1.183 1.251 1.454.66.253 1.521.389 2.586.389.98 0 1.69-.102 2.164-.288.456-.186.693-.507.693-.93 0-.169-.034-.304-.085-.422a.7.7 0 0 0-.253-.287c-.254-.153-.676-.254-1.251-.305-.592-.067-1.437-.118-2.535-.169a16 16 0 0 1-2.705-.422 3.04 3.04 0 0 1-1.572-.947c-.338-.422-.507-1.014-.507-1.724a2.7 2.7 0 0 1 1.318-2.366 6.93 6.93 0 0 1 3.922-.896zm27.417 0c.625 0 1.217.05 1.758.169a7.5 7.5 0 0 1 1.555.456 4.99 4.99 0 0 1 3.127 4.8v.102c0 .554-.074 1.105-.22 1.64a4.5 4.5 0 0 1-.608 1.352 4.9 4.9 0 0 1-2.266 1.74 8.8 8.8 0 0 1-3.33.592q-.901.006-1.791-.135a7.7 7.7 0 0 1-1.572-.422 4.8 4.8 0 0 1-2.231-1.741 5.3 5.3 0 0 1-.795-3.043v-.068a4.92 4.92 0 0 1 3.094-4.817 8 8 0 0 1 3.279-.625m36.612.253v4.547c.005.753.131 1.501.372 2.215.236.54.591.93 1.065 1.149.857.346 1.795.44 2.704.27q.398-.073.761-.253c.456-.22.794-.609 1.014-1.15a5.75 5.75 0 0 0 .338-2.23v-4.548h2.654v4.615c0 .727-.068 1.369-.169 1.96a5.8 5.8 0 0 1-.524 1.522c-.457.879-1.116 1.521-1.978 1.91a9.2 9.2 0 0 1-4.817.44 7 7 0 0 1-1.454-.44 3.95 3.95 0 0 1-1.944-1.91c-.44-.88-.659-2.028-.659-3.482v-4.615zm-10.92-.253c1.742 0 3.144.304 4.226.93a4 4 0 0 1 2.012 2.94h-2.79c-.118-.557-.54-.98-1.064-1.267a4.94 4.94 0 0 0-2.282-.406 4.56 4.56 0 0 0-2.89.795c-.677.507-1.015 1.369-1.015 2.535v.017c.051 1.37.474 1.86.947 2.316 1.065.997 5.764 1.487 6.305-.846h-3.516v-2.01h6.322v1.757c0 1.352-1.319 4.057-5.714 4.057-.574 0-2.248.135-3.989-.676a5.07 5.07 0 0 1-2.975-4.615v-.068c0-.574.085-1.115.22-1.605.13-.48.336-.937.609-1.353.557-.811 1.335-1.436 2.298-1.859a8.05 8.05 0 0 1 3.313-.676zm24.544.253 6.491 7.015.101.135v-7.15h2.417V37.13h-2.417l-6.592-7.065-.085-.152v7.217h-2.434V26.82zm-32.167 0v2.08H78.03v8.231h-2.586v-8.232h-3.837V26.82zm-35.04 0v4.006h6.22V26.82h2.502V37.13h-2.502v-3.989h-6.22v3.99h-2.535V26.818zm16.988 1.944c-1.184 0-2.113.254-2.79.778q-1.013.76-1.014 2.535c0 1.099.338 1.893 1.015 2.4.676.508 1.605.761 2.772.761q.56.007 1.115-.068c.338-.05.66-.135.93-.236.575-.186 1.014-.507 1.318-.98.322-.457.474-1.082.474-1.86a4.4 4.4 0 0 0-.119-1.099 2.64 2.64 0 0 0-1.673-1.91c-.558-.22-1.234-.338-2.028-.338z"
clipRule="evenodd"
/>
</svg>
)
}
export function SoshiLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 148 60" {...props}>
<path
fill="currentColor"
d="M24.823 10.698c4.452-1.25 8.044-.547 10.153 0s7.184 1.405 11.323 2.889 5.076 3.983 5.076 6.013.078 12.807.078 16.712-.468 5.858-2.343 6.873-10.62 4.92-13.354 6.013-5.701.937-7.497.313c-1.797-.626-12.73-4.608-15.229-5.545s-2.89-3.67-2.89-6.873V22.96c0-5.857 1.172-7.576 3.437-8.435s6.795-2.576 11.246-3.826m3.075 24.824-.811.01-.155.313c-.064.573-.049.857.253 1.356.776.756 1.454.882 2.5.892.455-.077.67-.205.995-.53.108-.424.074-.822 0-1.25-.227-.351-.37-.436-.762-.566l-.332-.059a6.3 6.3 0 0 0-1.232-.172zm-8.62-1.24c0 .469 0 .469.254.772l.37.322c.453.407.784.608 1.393.675.987.016 1.623-.012 2.395-.656.274-.332.274-.332.274-.957l-.186.195-.283.274-.303.342c-.533.315-.804.336-1.415.351-.969 0-1.465-.295-2.158-.957zm8.108-5.847c-1.35 0-2.444 1.45-2.444 3.239s1.095 3.238 2.444 3.238 2.442-1.45 2.442-3.238-1.093-3.238-2.442-3.239m-13.262 3.66c-.619-.088-1.077-.112-1.406.47.016.76.257 1.178.781 1.717.64.512 1.376.657 2.186.625.47-.312.47-.313.532-.605l-.004-.333.004-.331c-.062-.293-.063-.293-.531-.605a16 16 0 0 1-.938-.312q-.303-.323-.624-.625m2.348-6.124c-1.35 0-2.443 1.45-2.443 3.238s1.094 3.237 2.443 3.237 2.443-1.45 2.443-3.237-1.094-3.238-2.443-3.238m10.592 3.38c.451 0 .817.44.817.984 0 .543-.366.984-.817.984s-.816-.44-.816-.984.366-.984.816-.984m1.567 1.123a.423.423 0 1 1 0 .847.423.423 0 0 1 0-.847m-12.48-3.587c.45 0 .816.44.816.983s-.365.984-.816.984-.816-.44-.816-.984c0-.543.366-.983.816-.983m1.567 1.122a.423.423 0 0 1 0 .845.422.422 0 1 1 0-.844M70.253 41.384q-2.271 0-3.936-.704-1.632-.736-2.592-1.984a5.36 5.36 0 0 1-1.056-2.816h4.768q.128.544.448.992.352.416.928.672.608.224 1.344.224.8 0 1.28-.192.511-.224.768-.576.256-.352.256-.736 0-.608-.384-.928-.352-.32-1.056-.512-.704-.224-1.696-.416a46 46 0 0 1-2.304-.576 10.3 10.3 0 0 1-2.016-.864 4.4 4.4 0 0 1-1.408-1.312q-.512-.833-.512-2.016 0-1.44.8-2.592.8-1.184 2.304-1.856 1.505-.705 3.648-.704 3.04 0 4.768 1.344t2.048 3.616h-4.48a1.55 1.55 0 0 0-.8-.96q-.608-.352-1.536-.352-1.056 0-1.6.352-.543.352-.544.928 0 .383.352.704.384.288 1.088.512t1.728.448q1.952.416 3.36.896 1.44.48 2.24 1.408.801.896.768 2.624.032 1.568-.832 2.784-.833 1.215-2.4 1.92-1.568.672-3.744.672m16.225 0q-2.335 0-4.192-1.088a8.04 8.04 0 0 1-2.944-2.976q-1.088-1.92-1.088-4.352 0-2.496 1.088-4.384a8 8 0 0 1 2.944-3.008q1.888-1.088 4.192-1.088 2.368 0 4.224 1.088a8 8 0 0 1 2.944 3.008q1.089 1.889 1.088 4.352t-1.088 4.384a8.04 8.04 0 0 1-2.944 2.976q-1.855 1.088-4.224 1.088m0-4.16q.96 0 1.696-.48.769-.48 1.216-1.44.448-.96.448-2.368t-.448-2.368-1.184-1.44-1.696-.48-1.728.48q-.735.48-1.184 1.44-.447.96-.448 2.368 0 1.408.448 2.368.449.96 1.184 1.44.768.48 1.696.48m16.584 4.16q-2.273 0-3.936-.704-1.632-.736-2.592-1.984a5.36 5.36 0 0 1-1.056-2.816h4.768q.128.544.448.992.352.416.928.672.607.224 1.344.224.8 0 1.28-.192.511-.224.768-.576.256-.352.256-.736 0-.608-.384-.928-.352-.32-1.056-.512a15 15 0 0 0-1.696-.416 46 46 0 0 1-2.304-.576 10.2 10.2 0 0 1-2.016-.864 4.4 4.4 0 0 1-1.408-1.312q-.513-.833-.512-2.016 0-1.44.8-2.592.8-1.184 2.304-1.856 1.504-.705 3.648-.704 3.04 0 4.768 1.344t2.048 3.616h-4.48a1.56 1.56 0 0 0-.8-.96q-.608-.352-1.536-.352-1.056 0-1.6.352t-.544.928q0 .383.352.704.384.288 1.088.512t1.728.448q1.951.416 3.36.896 1.44.48 2.24 1.408.8.896.768 2.624.032 1.568-.832 2.784-.833 1.215-2.4 1.92-1.567.672-3.744.672m8.481-.384V17.96h4.8v9.216a5.7 5.7 0 0 1 2.08-1.952q1.313-.736 3.072-.736 2.048 0 3.424.864t2.08 2.528q.704 1.632.704 3.968V41h-4.768v-8.704q0-1.793-.768-2.784-.736-.992-2.368-.992-.96 0-1.76.48t-1.248 1.376q-.448.864-.448 2.08V41zm18.14 0V24.872h4.8V41zm2.4-17.984q-1.28 0-2.08-.736t-.8-1.856q0-1.152.8-1.888.8-.735 2.08-.736 1.313 0 2.112.736.832.736.832 1.888 0 1.12-.832 1.856-.8.735-2.112.736"
/>
</svg>
)
}
export function SubmagicLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 150 52" {...props}>
<path
fill="currentColor"
d="M34.73 31.72c-.593.597-1.352.903-2.196 1.038-.95.15-1.877.03-2.797-.17-.44-.093-.865-.255-1.335-.398l.793-1.323c.375.16.725.321 1.08.462.69.273 1.407.42 2.154.391 1.218-.045 2.089-.628 2.634-1.689.33-.642.456-1.328.39-2.043-.018-.198-.092-.391-.134-.588-.027-.127.102-.24.318-.3.147-.042.193.05.221.156.459 1.692.123 3.203-1.127 4.463l-.002.001Zm1.59-16.22H16.572c-2.667 0-4.848 2.148-4.848 4.774v7.361q.64-.066 1.28-.092c.835-.032 1.671-.069 2.503-.035 1.013.043 2.027.137 3.036.244.767.083 1.536.187 2.29.343 1.097.227 2.19.476 3.268.777.865.242 1.705.57 2.554.869.144.05.236.04.329-.087.897-1.227 2.838-3.913 2.838-3.913l-2.495-.305 2.258-2.38-.606-3.04 2.904 1.371 2.799-1.502-.455 3.099 2.407 2.196-3.25.538-1.336 2.933-1.187-2.2s-7.445 11.378-8.618 13.174c-.519.794-.526.8-1.039 1.6-.08.124-.137.155-.282.053-.49-.348-.994-.676-1.495-1.011-.177-.12-.187-.176-.064-.342.968-1.314 6.246-8.43 6.277-8.49-.241-.055-.463-.123-.69-.158-.882-.14-1.764-.297-2.651-.394-.82-.089-1.648-.13-2.473-.155a27 27 0 0 0-2.254.011c-.726.037-1.45.12-2.17.211a35 35 0 0 0-1.973.323q-.649.123-1.28.305c-.467.132-.927.282-1.387.435q-.386.129-.762.279c.533.307.965.742 1.262 1.253a3.4 3.4 0 0 1 .462 1.705v4.476c0 2.626 2.18 4.774 4.848 4.774H36.32c2.665 0 4.846-2.148 4.846-4.774V20.274c0-2.626-2.18-4.774-4.846-4.774m10.846 17.984 1.578-2.283c.98 1.036 2.677 2.048 4.445 2.048.932 0 1.483-.283 1.483-.894 0-.61-.67-.847-1.913-1.153l-1.578-.377c-2.102-.542-3.633-1.366-3.633-3.39 0-2.26 1.864-3.601 4.732-3.601 2.533 0 4.374 1.082 5.186 2.095l-1.53 2.26c-1.147-1.178-2.58-1.814-3.943-1.814-.836 0-1.338.305-1.338.871 0 .613.502.848 2.056 1.272l1.315.353c2.58.683 3.704 1.67 3.704 3.436 0 2.237-1.912 3.508-4.636 3.508s-4.924-.941-5.928-2.33Zm11.758-1.389v-8.051h3.417v6.874c0 1.412.383 2.283 1.96 2.283s2.126-.894 2.126-2.777v-6.38h3.418v11.535h-3.418v-3.225c-.406 1.836-1.53 3.46-3.896 3.46-2.222 0-3.609-1.342-3.609-3.72h.002Zm13.095 3.484V19.808h3.417v7.533c.334-1.86 1.29-3.531 3.729-3.531 2.724 0 4.516 2.19 4.516 6.003s-1.792 6.026-4.66 6.026c-2.439 0-3.275-1.647-3.585-3.506v3.249h-3.418zm5.64-2.378c1.553 0 2.532-1.177 2.532-3.39s-.98-3.39-2.533-3.39c-1.433 0-2.222.894-2.222 2.778v1.247c0 1.884.789 2.754 2.222 2.754v.001Zm7.575 2.378V24.044h3.417v3.32c.359-1.884 1.41-3.555 3.823-3.555 2.151 0 3.346 1.341 3.346 3.578.359-1.906 1.41-3.578 3.823-3.578 2.151 0 3.346 1.341 3.346 3.719v8.05h-3.417v-6.873c0-1.506-.478-2.284-1.864-2.284s-1.888.894-1.888 2.778v6.38h-3.417v-6.874c0-1.506-.478-2.284-1.864-2.284s-1.888.894-1.888 2.778v6.38zm19.332-2.566c0-1.742 1.123-2.755 3.346-3.509l3.584-1.317c-.12-.966-.573-1.507-1.864-1.507-1.482 0-2.844.824-3.657 1.978l-1.721-2.448a7.7 7.7 0 0 1 5.617-2.401c2.963 0 4.684 1.554 4.684 4.448v3.955c0 .518.191.682.622.682h.693v2.684h-2.008c-1.267 0-2.414-.565-2.414-2.214v-.777c-.383 1.507-1.41 3.25-3.801 3.25-1.816 0-3.083-.966-3.083-2.824zm4.349.353c1.625 0 2.533-.8 2.533-2.213v-.801l-2.486 1.012c-.932.377-1.243.705-1.243 1.153 0 .542.383.847 1.194.847zm10.609-1.671c-.167.259-.31.612-.31.942 0 .542.287.847 1.219.847h4.111c2.605 0 3.776 1.059 3.776 2.754 0 2.165-2.127 3.954-6.788 3.954-3.608 0-5.497-1.059-5.497-2.424 0-1.107 1.171-1.671 2.582-1.836-.981-.21-1.745-.729-1.745-1.741 0-1.177 1.052-2.143 2.175-2.754-1.219-.73-1.935-1.93-1.935-3.414 0-2.494 2.055-4.213 5.114-4.213.693 0 1.339.094 1.935.259l.526-2.777 3.036 1.012-2.27 2.331c1.195.753 1.912 1.93 1.912 3.39 0 2.494-2.079 4.213-5.138 4.213-1.028 0-1.935-.188-2.7-.542zm5.856 4.944c0-.518-.43-.683-1.339-.683h-4.947c-.144.165-.31.4-.31.683 0 .753.836 1.2 3.036 1.2 2.365 0 3.56-.447 3.56-1.2m-1.219-8.616c0-1.295-.836-1.953-1.935-1.953s-1.936.658-1.936 1.953c0 1.296.861 1.954 1.936 1.954s1.935-.658 1.935-1.954m5.328-5.343v-2.872h3.417v2.872zm0 12.899V24.044h3.417v11.535zm5.041-5.768c0-3.506 2.271-6.002 5.88-6.002 2.796 0 4.732 1.506 5.448 3.484l-2.844 1.624c-.287-1.436-1.052-2.307-2.485-2.307-1.505 0-2.51 1.083-2.51 3.201s1.003 3.202 2.51 3.202 2.199-.872 2.485-2.308l2.844 1.625c-.716 1.978-2.652 3.484-5.448 3.484-3.609 0-5.88-2.472-5.88-6.003"
/>
</svg>
)
}
export function TempoLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 191 60" {...props}>
<g fill="currentColor" clipPath="url(#a)">
<path
fillRule="evenodd"
d="M19.46 8.274a8.69 8.69 0 0 0-8.691 8.69v26.072c0 4.8 3.89 8.69 8.69 8.69h26.072c4.799 0 8.69-3.89 8.69-8.69V16.965c0-4.8-3.891-8.69-8.69-8.69zm24.084 12.932 1.987-2.793H19.459l1.987 2.793h9.543l-.021 22.422 1.419 1.863 1.418-1.863.021-22.422zM27.659 38.897l2.364 2.794V22.137H22.11l1.655 2.328h3.905zm7.093 2.794 2.355-2.794V24.465h4.12l1.655-2.328h-8.13z"
clipRule="evenodd"
/>
<path d="M73.77 42.601v-21.52h-7.98V17.66h19.703v3.42h-7.981v21.521zM93.816 42.922c-5.63 0-9.3-4.133-9.3-9.478 0-5.416 3.706-9.513 9.264-9.513 5.38 0 8.765 4.24 8.765 9.085v1.675h-14.43c.463 3.492 2.815 5.024 5.7 5.024 2.922 0 4.811-1.782 5.381-3.385l2.744 1.46c-.784 2.032-3.278 5.132-8.124 5.132m-5.594-11.224H98.84c-.036-2.28-1.96-4.632-5.096-4.632-2.707 0-4.917 1.604-5.522 4.632M106.284 42.601v-18.35h3.563v2.316c.855-1.603 2.779-2.636 4.988-2.636 2.459 0 4.454 1.14 5.666 2.85.926-1.211 2.743-2.85 5.879-2.85 4.24 0 7.233 3.064 7.233 6.912v11.758h-3.563V31.698c0-2.743-1.746-4.418-4.133-4.418-2.459 0-4.205 1.817-4.205 4.418v10.903h-3.563V31.698c0-2.743-1.71-4.418-4.133-4.418-2.459 0-4.169 1.817-4.169 4.418v10.903zM137.879 50.76V24.251h3.563v2.352c1.176-1.568 3.385-2.672 6.129-2.672 4.988 0 8.943 3.563 8.943 9.513 0 5.523-3.884 9.478-8.943 9.478-2.744 0-5.06-1.176-6.129-2.744V50.76zm3.492-17.316c0 3.35 2.209 6.164 5.772 6.164s5.772-2.78 5.772-6.164c0-3.42-2.209-6.2-5.772-6.2s-5.772 2.815-5.772 6.2M169.036 42.922c-5.63 0-9.656-3.99-9.656-9.478s4.062-9.513 9.656-9.513 9.656 4.026 9.656 9.513c0 5.451-4.027 9.478-9.656 9.478m-6.057-9.478c0 3.634 2.672 6.2 6.057 6.2 3.42 0 6.057-2.566 6.057-6.2s-2.637-6.2-6.057-6.2-6.057 2.566-6.057 6.2" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M10.769 8.274h169.462v43.452H10.769z" />
</clipPath>
</defs>
</svg>
)
}
export function UdioLogo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 86 60" {...props}>
<path
fill="currentColor"
d="M41.436 24.392c-1.133-.886-2.678-1.394-4.359-1.394-4.734 0-8.132 3.675-8.132 8.612s3.396 8.613 8.132 8.613c1.715 0 3.293-.58 4.462-1.532v1.19h5.146V15.167l-5.25 1.296zm-3.432 11.131c-2.162 0-3.74-1.634-3.74-3.915 0-2.28 1.578-3.916 3.774-3.916s3.704 1.6 3.704 3.916-1.508 3.915-3.74 3.915zM55.052 22.816l-5.25 1.297v15.765h5.25zM52.444 15.335c-1.648 0-2.848 1.26-2.848 2.86 0 1.602 1.2 2.861 2.848 2.861s2.813-1.259 2.813-2.86c0-1.602-1.166-2.86-2.813-2.86M66.174 22.962c-5.215 0-9.125 3.711-9.125 8.613 0 4.9 3.946 8.681 9.125 8.681s9.126-3.744 9.126-8.681c0-4.938-3.946-8.613-9.126-8.613m0 12.561c-2.229 0-3.773-1.599-3.773-3.915 0-2.317 1.544-3.916 3.773-3.916s3.774 1.566 3.774 3.88-1.508 3.949-3.774 3.949zM21.9 32.867c0 1.737-.96 2.758-2.538 2.758h-3.293v-12.29H10.82v7.08l5.25 5.21h-5.25l5.25 5.208v-5.208l4.58 4.545c3.894-.506 6.5-3.33 6.5-7.267v-9.568H21.9z"
/>
</svg>
)
}
export function V0Logo(props: ComponentProps<'svg'>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 101 60" {...props}>
<g fill="currentColor" clipPath="url(#a)">
<path d="M57.015 10h19.054c7.726 0 13.99 6.263 13.99 13.99V42.16h-7.827V23.99a7 7 0 0 0-.022-.555L63.158 42.158q.096.003.193.003h18.882v7.391H63.35c-7.726 0-14.162-6.324-14.162-14.05V17.378h7.827v18.124q0 .523.08 1.032l19.472-19.135a6 6 0 0 0-.498-.02H57.015zM37.765 48.191 10.228 17.375h11.078l16.168 18.093V17.375h8.26V45.15c0 4.192-5.176 6.168-7.969 3.042" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M10.228 10h80v40h-80z" />
</clipPath>
</defs>
</svg>
)
}
+19
View File
@@ -0,0 +1,19 @@
'use client'
import Footer from '~/components/Footer'
import Nav from '~/components/Nav'
import { useForceDeepDark } from 'lib/theme.utils'
export default function HomeLayout({ children }: { children: React.ReactNode }) {
useForceDeepDark()
return (
<>
<Nav hideNavbar={false} />
<div className="relative w-full [--container-max-w:75rem]">
<main className="relative min-h-screen">{children}</main>
</div>
<Footer />
</>
)
}
+6
View File
@@ -0,0 +1,6 @@
import { FrameworksSection } from './_components/FrameworksSection'
import { HomeContent } from './_components/HomeContent'
export default function HomePage() {
return <HomeContent frameworksSlot={<FrameworksSection />} />
}
@@ -0,0 +1,51 @@
import Image from 'next/image'
import Link from 'next/link'
interface QuoteSectionProps {
quote: string
highlight?: string
author: {
name: string
role: string
image: string
link: string
}
}
export function QuoteSection({ quote, highlight, author }: QuoteSectionProps) {
return (
<div>
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border bg-surface-200">
<div className="flex flex-col items-center text-center gap-8 md:gap-12 py-16 md:py-24">
<q className="text-2xl xl:text-4xl tracking-tight max-w-screen-lg text-foreground-lighter text-balance">
{quote}
{highlight && (
<>
{' '}
<span className="text-brand">{highlight}</span>
</>
)}
</q>
<Link href={author.link} className="hover:opacity-90 transition-opacity">
<div className="flex flex-row items-center gap-3">
<Image
draggable={false}
src={author.image}
alt={`${author.name}, ${author.role}`}
className="w-12 h-12 rounded-full overflow-hidden object-cover"
width={48}
height={48}
/>
<div className="flex flex-col items-start gap-0.5">
<span className="text-foreground text-sm">{author.name}</span>
<span className="text-foreground-lighter text-xs">{author.role}</span>
</div>
</div>
</Link>
</div>
</div>
</div>
)
}
@@ -0,0 +1,324 @@
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { ApiSectionClient } from './ApiSectionClient'
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#bda4ff' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#3ecf8e' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#3ecf8e' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#3ecf8e' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#ffffff' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
],
}
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#6b35dc' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#15593b' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#15593b' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#15593b' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#525252' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
],
}
const LANG_MAP: Record<string, string> = {
JavaScript: 'javascript',
Flutter: 'dart',
Python: 'python',
'C#': 'csharp',
Kotlin: 'kotlin',
Swift: 'swift',
}
const API_EXAMPLES = [
{
icon: 'UserPlus' as const,
title: 'Sign up',
description: 'Create new accounts with email/password, phone, or third-party providers.',
code: {
JavaScript: `// Sign up with email and password
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
})`,
Flutter: `// Sign up with email and password
final response = await supabase.auth.signUp(
email: 'user@example.com',
password: 'secure-password',
);`,
Python: `# Sign up with email and password
response = supabase.auth.sign_up({
"email": "user@example.com",
"password": "secure-password",
})`,
'C#': `// Sign up with email and password
var session = await supabase.Auth.SignUp(
"user@example.com",
"secure-password"
);`,
Kotlin: `// Sign up with email and password
supabase.auth.signUpWith(Email) {
email = "user@example.com"
password = "secure-password"
}`,
Swift: `// Sign up with email and password
try await supabase.auth.signUp(
email: "user@example.com",
password: "secure-password"
)`,
},
},
{
icon: 'LogIn' as const,
title: 'Sign in',
description: 'Authenticate users with multiple strategies in a single call.',
code: {
JavaScript: `// Sign in with email
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password',
})
// Sign in with magic link
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
})`,
Flutter: `// Sign in with email
final response = await supabase.auth.signInWithPassword(
email: 'user@example.com',
password: 'secure-password',
);
// Sign in with magic link
await supabase.auth.signInWithOtp(
email: 'user@example.com',
);`,
Python: `# Sign in with email
response = supabase.auth.sign_in_with_password({
"email": "user@example.com",
"password": "secure-password",
})
# Sign in with magic link
response = supabase.auth.sign_in_with_otp({
"email": "user@example.com",
})`,
'C#': `// Sign in with email
var session = await supabase.Auth.SignIn(
"user@example.com",
"secure-password"
);
// Sign in with magic link
await supabase.Auth.SignIn(
Constants.SignInType.MagicLink,
"user@example.com"
);`,
Kotlin: `// Sign in with email
supabase.auth.signInWith(Email) {
email = "user@example.com"
password = "secure-password"
}
// Sign in with magic link
supabase.auth.signInWith(OTP) {
email = "user@example.com"
}`,
Swift: `// Sign in with email
try await supabase.auth.signIn(
email: "user@example.com",
password: "secure-password"
)
// Sign in with magic link
try await supabase.auth.signInWithOTP(
email: "user@example.com"
)`,
},
},
{
icon: 'Globe' as const,
title: 'OAuth logins',
description: 'Social login with 20+ providers and custom scopes for extended permissions.',
code: {
JavaScript: `// Sign in with GitHub
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
scopes: 'repo gist notifications',
},
})`,
Flutter: `// Sign in with GitHub
await supabase.auth.signInWithOAuth(
OAuthProvider.github,
scopes: 'repo gist notifications',
);`,
Python: `# Sign in with GitHub
response = supabase.auth.sign_in_with_oauth({
"provider": "github",
"options": {
"scopes": "repo gist notifications",
},
})`,
'C#': `// Sign in with GitHub
var session = await supabase.Auth.SignIn(
Constants.Provider.Github,
new SignInOptions {
Scopes = "repo gist notifications"
}
);`,
Kotlin: `// Sign in with GitHub
supabase.auth.signInWith(Github) {
scopes = "repo gist notifications"
}`,
Swift: `// Sign in with GitHub
try await supabase.auth.signInWithOAuth(
provider: .github,
scopes: "repo gist notifications"
)`,
},
},
{
icon: 'KeyRound' as const,
title: 'Session management',
description: 'Retrieve, refresh, and manage user sessions with built-in token handling.',
code: {
JavaScript: `// Get the current user
const { data: { user } } = await supabase.auth.getUser()
// Listen for auth state changes
supabase.auth.onAuthStateChange((event, session) => {
console.log(event, session)
})`,
Flutter: `// Get the current user
final user = supabase.auth.currentUser;
// Listen for auth state changes
supabase.auth.onAuthStateChange.listen((data) {
final event = data.event;
final session = data.session;
});`,
Python: `# Get the current user
user = supabase.auth.get_user()
# Get the current session
session = supabase.auth.get_session()`,
'C#': `// Get the current user
var user = supabase.Auth.CurrentUser;
// Listen for auth state changes
supabase.Auth.AddStateChangedListener(
(sender, changed) => {
// Handle state change
}
);`,
Kotlin: `// Get the current user
val user = supabase.auth.currentUserOrNull()
// Listen for auth state changes
supabase.auth.sessionStatus.collect { status ->
when (status) {
is SessionStatus.Authenticated -> { }
SessionStatus.NotAuthenticated -> { }
}
}`,
Swift: `// Get the current user
let user = try await supabase.auth.user()
// Listen for auth state changes
for await (event, session) in supabase.auth.authStateChanges {
print(event, session)
}`,
},
},
]
export async function ApiSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: Object.values(LANG_MAP),
})
const examples = API_EXAMPLES.map((example) => ({
icon: example.icon,
title: example.title,
description: example.description,
languages: Object.fromEntries(
Object.entries(example.code).map(([lang, code]) => [
lang,
{
darkHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-light' }),
},
])
),
}))
hl.dispose()
return <ApiSectionClient examples={examples} />
}
@@ -0,0 +1,222 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronDown, Globe, KeyRound, LogIn, UserPlus } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { UserPlus, LogIn, Globe, KeyRound } as const
type IconName = keyof typeof ICONS
type ApiExample = {
icon: IconName
title: string
description: string
languages: Record<string, { darkHtml: string; lightHtml: string }>
}
const LANGUAGES = ['JavaScript', 'Flutter', 'Python', 'C#', 'Kotlin', 'Swift']
const INTERVAL_DURATION = 8000
export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [activeLang, setActiveLang] = useState('JavaScript')
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = examples[activeIdx]
// Auto-size the code panel to the active snippet. We measure an always-mounted,
// invisible copy of the current code so the target height is known immediately on
// switch (and doesn't collapse during the cross-fade), then animate `height` to it.
const measureRef = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState<number>()
const hasMeasured = useRef(false)
useEffect(() => {
const el = measureRef.current
if (!el) return
const ro = new ResizeObserver(() => setHeight(el.offsetHeight))
ro.observe(el)
setHeight(el.offsetHeight)
return () => ro.disconnect()
}, [])
useEffect(() => {
if (height != null) hasMeasured.current = true
}, [height])
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % examples.length)
}
}, [progress, examples.length])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Simple APIs
<br />
<span className="text-foreground">powerful libraries</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
APIs that you can understand. With powerful libraries that work on client and
server-side applications.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tab cards */}
<div className="flex flex-col gap-1 items-start justify-end">
{examples.map((example, index) => {
const isActive = index === activeIdx
const Icon = ICONS[example.icon]
return (
<button
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{example.title}
</button>
)
})}
</div>
{/* Right: code block */}
<motion.div
initial={false}
animate={height != null ? { height } : undefined}
transition={
hasMeasured.current ? { duration: 0.35, ease: [0.22, 1, 0.36, 1] } : { duration: 0 }
}
className="relative self-end overflow-hidden border border-border rounded-md"
>
{/* Invisible measuring copy: drives the animated container height */}
<div
ref={measureRef}
aria-hidden
className="pointer-events-none invisible absolute inset-x-0 top-0 overflow-x-auto pb-12 [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: active.languages[activeLang]?.darkHtml ?? '' }}
/>
{/* Language switcher */}
<div className="absolute top-3 right-4 z-10 flex items-center">
<div className="relative flex items-center gap-1 text-foreground-muted text-xs">
<select
value={activeLang}
onChange={(e) => setActiveLang(e.target.value)}
className="appearance-none bg-transparent text-foreground-light text-xs pr-4 cursor-pointer border-none outline-none shadow-none focus:border-none focus:outline-none focus:shadow-none focus:ring-0"
>
{LANGUAGES.map((lang) => (
<option key={lang} value={lang}>
{lang}
</option>
))}
</select>
<ChevronDown size={12} className="absolute right-0 pointer-events-none" />
</div>
</div>
<AnimatePresence mode="wait">
<motion.div
key={`${active.title}-${activeLang}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="overflow-x-auto pb-12"
>
<div
dangerouslySetInnerHTML={{ __html: active.languages[activeLang]?.darkHtml ?? '' }}
className="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
<div
dangerouslySetInnerHTML={{
__html: active.languages[activeLang]?.lightHtml ?? '',
}}
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
<Link
href="/docs/guides/auth"
className="absolute bottom-4 right-4 flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap"
>
Documentation
<svg
width={12}
height={12}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0"
>
<path
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</motion.div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,39 @@
import { BuiltWithSupabaseSection } from '~/app/(home)/_components/BuiltWithSupabaseSection'
import { CTASection } from '~/app/(home)/_components/CTASection'
import { FeaturesSection } from './FeaturesSection'
import { Hero } from './Hero'
import { HighlightsSection } from './HighlightsSection'
import { QuoteSection } from './QuoteSection'
export function AuthContent({
apiSlot,
rlsSlot,
}: {
apiSlot: React.ReactNode
rlsSlot: React.ReactNode
}) {
return (
<div className="overflow-x-clip">
<section>
<Hero />
</section>
<section className="border-t border-border">
<FeaturesSection />
</section>
<section className="border-t border-border">
<QuoteSection />
</section>
<section className="border-t border-border">
<HighlightsSection />
</section>
<section className="border-t border-border">{apiSlot}</section>
<section className="border-t border-border">{rlsSlot}</section>
<section className="border-t border-border">
<BuiltWithSupabaseSection />
</section>
<section className="border-t border-border">
<CTASection />
</section>
</div>
)
}
@@ -0,0 +1,604 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { Link as LinkIcon } from 'lucide-react'
import Image from 'next/image'
import { useEffect, useRef, useState } from 'react'
import { cn } from 'ui'
import { ShieldGlow } from './Shaders/ShieldGlow'
const PROVIDERS = [
'github',
'google',
'apple',
'discord',
'facebook',
'gitlab',
'microsoft',
'twitter',
'twitch',
'spotify',
'slack',
'bitbucket',
'twilio',
'solana',
'ethereum',
]
const FEATURES = [
{
title: 'All the social providers',
description:
'Add social logins with one click. Google, Facebook, GitHub, Azure (Microsoft), GitLab, Twitter, Discord, and many more.',
visual: ProvidersSkeleton,
},
{
title: 'Fully integrated',
description:
'Auth without any external services. Built-in authentication, authorization, and user management.',
visual: IntegratedSkeleton,
},
{
title: 'Own your data',
description:
'User data stored in your Supabase database. No third-party privacy concerns. Host in 17+ locations.',
visual: DataOwnershipSkeleton,
},
]
const COLS = 13
const ROWS = 13
const CELL_SIZE = 48
const GAP = 10
function ProvidersSkeleton() {
const [activeCell, setActiveCell] = useState(Math.floor((COLS * ROWS) / 2))
const prevCell = useRef(activeCell)
const containerRef = useRef<HTMLDivElement>(null)
const [isInView, setIsInView] = useState(false)
const totalCells = COLS * ROWS
const grid = Array.from({ length: totalCells }, (_, i) => PROVIDERS[i % PROVIDERS.length])
// Keep highlight away from edges so grid always overflows the card
const MARGIN = 3
const pickNext = () => {
const prev = prevCell.current
const prevCol = prev % COLS
const prevRow = Math.floor(prev / COLS)
let next = prev
const prevProvider = PROVIDERS[prev % PROVIDERS.length]
// Try until we get a cell 5+ tiles away and a different provider
for (let attempt = 0; attempt < 50; attempt++) {
const col = MARGIN + Math.floor(Math.random() * (COLS - MARGIN * 2))
const row = MARGIN + Math.floor(Math.random() * (ROWS - MARGIN * 2))
const candidate = row * COLS + col
const dist = Math.abs(col - prevCol) + Math.abs(row - prevRow)
if (dist >= 5 && PROVIDERS[candidate % PROVIDERS.length] !== prevProvider) {
next = candidate
break
}
}
prevCell.current = next
return next
}
// Compute offset to center the active cell in the viewport
const activeCol = activeCell % COLS
const activeRow = Math.floor(activeCell / COLS)
const centerCol = Math.floor(COLS / 2)
const centerRow = Math.floor(ROWS / 2)
const offsetX = (centerCol - activeCol) * (CELL_SIZE + GAP)
const offsetY = (centerRow - activeRow) * (CELL_SIZE + GAP)
useEffect(() => {
const el = containerRef.current
if (!el) return
const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), {
threshold: 0.3,
})
observer.observe(el)
return () => observer.disconnect()
}, [])
useEffect(() => {
if (!isInView) return
const timer = setInterval(() => setActiveCell(pickNext()), 5000)
return () => clearInterval(timer)
}, [isInView])
return (
<div
ref={containerRef}
role="img"
aria-label="A grid of social login provider icons — Google, GitHub, Apple, Discord, and more — with one highlighted at a time"
className="flex items-center justify-center w-full h-full relative overflow-hidden pointer-events-none select-none"
style={{
maskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, black 30%, transparent 75%)',
WebkitMaskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, black 30%, transparent 75%)',
}}
>
<div
className="grid transition-transform duration-1000 ease-in-out"
style={{
gridTemplateColumns: `repeat(${COLS}, ${CELL_SIZE}px)`,
gridTemplateRows: `repeat(${ROWS}, ${CELL_SIZE}px)`,
gap: `${GAP}px`,
transform: `translate(${offsetX}px, ${offsetY}px)`,
}}
>
{grid.map((provider, i) => {
const isActive = i === activeCell
return (
<div
key={i}
className={cn(
'flex items-center justify-center w-12 h-12 rounded-lg border transition-all duration-1000',
isActive ? 'bg-surface-200 border-border scale-110' : 'bg-surface-75 border-border'
)}
>
<Image
src={`/images/product/auth/${provider}-icon.svg`}
alt={provider}
width={20}
height={20}
className={cn(
'transition-all duration-1000',
isActive ? 'opacity-100 grayscale-0' : 'opacity-40 grayscale'
)}
/>
</div>
)
})}
</div>
</div>
)
}
function IntegratedSkeleton() {
return (
<div
role="img"
aria-label="Preview of Supabase Auth UI components — magic link sign-in, profile, organization setup, email verification, sign-up, password reset, account chooser, session tokens, and two-factor authentication"
className="relative flex h-full w-full items-center justify-center overflow-hidden pointer-events-none select-none"
style={{
maskImage:
'linear-gradient(to bottom, transparent, black 15%, black 65%, transparent), linear-gradient(to right, transparent, black 20%, black 80%, transparent)',
WebkitMaskImage:
'linear-gradient(to bottom, transparent, black 15%, black 65%, transparent), linear-gradient(to right, transparent, black 20%, black 80%, transparent)',
maskComposite: 'intersect',
WebkitMaskComposite: 'destination-in',
}}
>
<div className="flex items-start gap-3">
{/* Left column */}
<div className="flex w-[180px] shrink-0 flex-col gap-3 pt-4">
{/* Magic link card */}
<div className="rounded-xl border border-border bg-surface-100 p-4">
<div className="flex flex-col items-center gap-2">
<span className="text-[11px] font-medium text-foreground">
Sign in with magic link
</span>
<div className="flex h-6 w-full items-center rounded border border-border bg-surface-200 px-2">
<span className="text-[10px] text-foreground-muted">cameron@gmail.com</span>
</div>
<div className="flex h-6 w-full items-center justify-center rounded bg-foreground">
<span className="text-[9px] font-medium text-background">Send magic link</span>
</div>
</div>
</div>
{/* Profile card */}
<div className="rounded-xl border border-border bg-surface-100 p-4">
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand/15 text-[10px] font-medium text-brand">
CW
</div>
<div className="flex flex-col">
<span className="text-[11px] font-medium text-foreground">Cameron Walker</span>
<span className="text-[10px] text-foreground-muted">cameron@gmail.com</span>
</div>
</div>
<div className="flex flex-col gap-1.5">
<span className="text-[10px] text-foreground-muted">Name</span>
<div className="flex h-6 items-center rounded border border-border bg-surface-200 px-2">
<span className="text-[10px] text-foreground-muted">Cameron Walker</span>
</div>
</div>
<div className="flex flex-col gap-1.5">
<span className="text-[10px] text-foreground-muted">Phone number</span>
<div className="flex h-6 items-center rounded border border-border bg-surface-200 px-2">
<span className="text-[10px] text-foreground-muted">+1 (555) 000-0000</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-[10px] text-foreground-muted">Connected account</span>
<span className="text-[9px] text-brand">Google</span>
</div>
</div>
</div>
{/* Organization card */}
<div className="rounded-xl border border-border bg-surface-100 p-4">
<div className="flex flex-col gap-3">
<span className="text-[11px] font-medium text-foreground">Create organization</span>
<div className="flex flex-col gap-1.5">
<span className="text-[10px] text-foreground-muted">Name</span>
<div className="flex h-6 items-center rounded border border-border bg-surface-200 px-2">
<span className="text-[10px] text-foreground-muted">Acme Inc</span>
</div>
</div>
<div className="flex flex-col gap-1.5">
<span className="text-[10px] text-foreground-muted">Logo</span>
<div className="flex h-10 items-center justify-center rounded border border-dashed border-border bg-surface-200">
<span className="text-[9px] text-foreground-muted">Upload image</span>
</div>
</div>
</div>
</div>
</div>
{/* Center column */}
<div className="flex w-[220px] shrink-0 flex-col gap-3">
{/* Verify email card */}
<div className="rounded-xl border border-border bg-surface-100 p-4">
<div className="flex flex-col items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-brand/10">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="#3ECF8E"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M22 7l-10 6L2 7" />
</svg>
</div>
<span className="text-[11px] font-medium text-foreground">Verify your email</span>
<div className="flex gap-1">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="flex h-6 w-5 items-center justify-center rounded border border-border bg-surface-200 text-[10px] text-foreground-muted"
>
{i < 3 ? '•' : ''}
</div>
))}
</div>
</div>
</div>
{/* Sign up card */}
<div className="rounded-xl border border-border bg-surface-100 p-5 shadow-sm">
<div className="flex flex-col items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-foreground">
<svg width="16" height="17" viewBox="0 0 109 113" fill="none" aria-hidden>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="#249361"
/>
<path
d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z"
fill="#3ECF8E"
/>
</svg>
</div>
<div className="text-center">
<div className="text-xs font-medium text-foreground">Create your account</div>
<div className="mt-0.5 text-[10px] text-foreground-muted">
Welcome! Please fill in the details.
</div>
</div>
<div className="flex w-full flex-col gap-1.5">
<div className="flex h-7 items-center justify-center gap-1.5 rounded border border-border bg-surface-200">
<span className="text-[10px] text-foreground-muted">Continue with Google</span>
</div>
<div className="flex h-7 items-center justify-center gap-1.5 rounded border border-border bg-surface-200">
<span className="text-[10px] text-foreground-muted">Continue with GitHub</span>
</div>
</div>
</div>
</div>
{/* Password reset card */}
<div className="rounded-xl border border-border bg-surface-100 p-4">
<div className="flex flex-col items-center gap-2">
<span className="text-[11px] font-medium text-foreground">Reset your password</span>
<div className="flex flex-col gap-1 w-full">
<span className="text-[10px] font-medium text-foreground">New password</span>
<div className="flex h-6 items-center rounded border border-border bg-surface-200 px-2">
<span className="text-[10px] tracking-widest text-foreground-muted">
••••••••
</span>
</div>
</div>
<div className="flex h-6 w-full items-center justify-center rounded bg-foreground">
<span className="text-[9px] font-medium text-background">Update password</span>
</div>
</div>
</div>
</div>
{/* Right column */}
<div className="flex w-[180px] shrink-0 flex-col gap-3 pt-10">
{/* Account chooser card */}
<div className="rounded-xl border border-border bg-surface-100 p-4">
<div className="flex flex-col gap-3">
<div className="flex flex-col items-center gap-1">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-foreground">
<svg width="12" height="13" viewBox="0 0 109 113" fill="none" aria-hidden>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="#249361"
/>
<path
d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z"
fill="#3ECF8E"
/>
</svg>
</div>
<span className="text-[11px] font-medium text-foreground">Choose an account</span>
</div>
<div className="flex flex-col gap-1.5">
{[
{ name: 'Personal', role: '' },
{ name: 'Acme Inc', role: 'Admin' },
{ name: 'My Project', role: 'Member' },
].map((account) => (
<div
key={account.name}
className="flex items-center gap-2 rounded-lg border border-border bg-surface-200 px-2.5 py-1.5"
>
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-foreground-muted/15 text-[8px] text-foreground-muted">
{account.name[0]}
</div>
<div className="flex flex-col">
<span className="text-[10px] text-foreground">{account.name}</span>
{account.role && (
<span className="text-[8px] text-foreground-muted">{account.role}</span>
)}
</div>
</div>
))}
</div>
</div>
</div>
{/* Session card */}
<div className="rounded-xl border border-border bg-surface-100 p-4">
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] text-foreground-muted">Session</span>
{[
{ key: 'access_token', val: 'eyJhbG...kpXVc' },
{ key: 'token_type', val: 'bearer' },
{ key: 'user.email', val: 'cameron@gmail.com' },
{ key: 'user.role', val: 'authenticated' },
].map((row) => (
<div key={row.key} className="flex items-center justify-between">
<span className="font-mono text-[9px] text-brand">{row.key}</span>
<span className="ml-2 truncate font-mono text-[9px] text-foreground-muted">
{row.val}
</span>
</div>
))}
</div>
</div>
{/* MFA card */}
<div className="rounded-xl border border-border bg-surface-100 p-4">
<div className="flex flex-col gap-2.5">
<span className="text-[11px] font-medium text-foreground">Two-factor auth</span>
<div className="flex items-center justify-between rounded-lg border border-border bg-surface-200 px-2.5 py-2">
<div className="flex flex-col">
<span className="text-[10px] text-foreground">Authenticator app</span>
<span className="text-[9px] text-foreground-muted">TOTP via auth app</span>
</div>
<div className="rounded bg-brand/15 px-1.5 py-0.5 text-[8px] font-medium text-brand">
On
</div>
</div>
<div className="flex items-center justify-between rounded-lg border border-border bg-surface-200 px-2.5 py-2">
<div className="flex flex-col">
<span className="text-[10px] text-foreground">SMS verification</span>
<span className="text-[9px] text-foreground-muted">Code via text message</span>
</div>
<div className="rounded bg-foreground-muted/10 px-1.5 py-0.5 text-[8px] text-foreground-muted">
Off
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
function DataOwnershipSkeleton() {
const [hovered, setHovered] = useState(false)
return (
<div
role="img"
aria-label="A shield icon with a glowing green effect, representing user data ownership and security"
className="relative flex h-full w-full items-center justify-center select-none"
style={{
maskImage: 'radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 75%)',
WebkitMaskImage: 'radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 75%)',
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* Construction lines */}
<svg
className="absolute -inset-16 h-[calc(100%+128px)] w-[calc(100%+128px)] text-foreground-muted/20"
viewBox="0 0 480 480"
fill="none"
>
<rect
x="60"
y="40"
width="360"
height="400"
rx="48"
stroke="currentColor"
strokeWidth="0.5"
/>
<rect
x="30"
y="10"
width="420"
height="460"
rx="60"
stroke="currentColor"
strokeWidth="0.5"
/>
<rect
x="0"
y="-16"
width="480"
height="512"
rx="72"
stroke="currentColor"
strokeWidth="0.5"
/>
<rect
x="90"
y="70"
width="300"
height="340"
rx="36"
stroke="currentColor"
strokeWidth="0.5"
/>
<line x1="30" y1="10" x2="450" y2="470" stroke="currentColor" strokeWidth="0.5" />
<line x1="450" y1="10" x2="30" y2="470" stroke="currentColor" strokeWidth="0.5" />
<line x1="240" y1="0" x2="240" y2="480" stroke="currentColor" strokeWidth="0.5" />
<line x1="0" y1="240" x2="480" y2="240" stroke="currentColor" strokeWidth="0.5" />
<circle cx="240" cy="240" r="180" stroke="currentColor" strokeWidth="0.5" />
<circle cx="240" cy="240" r="140" stroke="currentColor" strokeWidth="0.5" />
<circle cx="240" cy="240" r="100" stroke="currentColor" strokeWidth="0.5" />
<line x1="60" y1="40" x2="60" y2="10" stroke="currentColor" strokeWidth="0.5" />
<line x1="60" y1="40" x2="30" y2="40" stroke="currentColor" strokeWidth="0.5" />
<line x1="420" y1="40" x2="450" y2="40" stroke="currentColor" strokeWidth="0.5" />
<line x1="420" y1="40" x2="420" y2="10" stroke="currentColor" strokeWidth="0.5" />
<line x1="60" y1="440" x2="30" y2="440" stroke="currentColor" strokeWidth="0.5" />
<line x1="60" y1="440" x2="60" y2="470" stroke="currentColor" strokeWidth="0.5" />
<line x1="420" y1="440" x2="450" y2="440" stroke="currentColor" strokeWidth="0.5" />
<line x1="420" y1="440" x2="420" y2="470" stroke="currentColor" strokeWidth="0.5" />
</svg>
{/* Highlighted lines on hover */}
<svg
className="absolute -inset-16 h-[calc(100%+128px)] w-[calc(100%+128px)] transition-opacity duration-500"
style={{ opacity: hovered ? 0.35 : 0 }}
viewBox="0 0 480 480"
fill="none"
>
<rect
x="60"
y="40"
width="360"
height="400"
rx="48"
stroke="hsl(var(--brand-default))"
strokeWidth="0.75"
opacity="0.4"
/>
<circle
cx="240"
cy="240"
r="140"
stroke="hsl(var(--brand-default))"
strokeWidth="0.75"
opacity="0.3"
/>
<line
x1="240"
y1="0"
x2="240"
y2="480"
stroke="hsl(var(--brand-default))"
strokeWidth="0.75"
opacity="0.25"
/>
<line
x1="0"
y1="240"
x2="480"
y2="240"
stroke="hsl(var(--brand-default))"
strokeWidth="0.75"
opacity="0.25"
/>
</svg>
{/* Shield-user icon + glow */}
<div className="relative h-44 w-44">
<ShieldGlow hovered={hovered} />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="relative z-[5] h-44 w-44"
strokeLinecap="round"
strokeLinejoin="round"
style={{
color: 'hsl(var(--brand-default))',
}}
>
<path
d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"
fill="none"
stroke="currentColor"
strokeWidth={0.25}
/>
</svg>
</div>
</div>
)
}
export function FeaturesSection() {
return (
<div className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Everything you need
<br />
<span className="text-foreground">for user authentication</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Social logins, email/password, magic links, phone auth, and more — with enterprise-grade
security built on Postgres&apos; Row Level Security.
</p>
</div>
</div>
{/* 3-col grid */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{FEATURES.map((feature) => {
const Visual = feature.visual
return (
<div
key={feature.title}
className="flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden"
>
<div className="relative flex items-center justify-center h-[320px]">
<Visual />
</div>
<div className="px-6 py-5 flex flex-col gap-1">
<h4 className="text-foreground text-sm font-medium">{feature.title}</h4>
<p className="text-foreground-lighter text-sm">{feature.description}</p>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
@@ -0,0 +1,29 @@
import Link from 'next/link'
import { Button } from 'ui'
export function Hero() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-32 pb-16 md:pt-40 md:pb-32">
<div className="flex flex-col gap-6 lg:gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h1 className="text-foreground text-3xl sm:text-5xl sm:leading-none">
<span className="block">Authentication</span>
<span className="text-foreground-lighter block">built in, not bolted on</span>
</h1>
<p className="text-foreground-lighter text-sm lg:text-base">
Every Supabase project comes with a complete user management system. Including
Postgres&apos; Row Level Security for fine-grained access control.
</p>
</div>
<div className="flex items-center gap-2">
<Button asChild size="medium">
<Link href="https://supabase.com/dashboard">Start your project</Link>
</Button>
<Button asChild size="medium" type="default">
<Link href="/docs/guides/auth">Documentation</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,81 @@
import { Check } from 'lucide-react'
import Link from 'next/link'
import { Button } from 'ui'
const HIGHLIGHTS = [
{
title: 'Multi-Factor Authentication',
features: [
'Time-based one-time passwords (TOTP)',
'Works with any authenticator app',
'Enforce MFA for specific user roles',
'Built-in enrollment and challenge flows',
],
cta: { label: 'Learn more', href: '/docs/guides/auth/auth-mfa' },
},
{
title: 'Enterprise SSO',
features: [
'SAML 2.0 support for identity providers',
'Connect to Azure AD, Okta, and more',
'Automatic user provisioning on sign-in',
'Manage SSO from the Dashboard or API',
],
cta: { label: 'Learn more', href: '/docs/guides/auth/enterprise-sso/auth-sso-saml' },
},
{
title: 'Custom OIDC Providers',
features: [
'OpenID Connect support for any identity provider',
'Connect to Auth0, Keycloak, Amazon Cognito, and more',
'PKCE security by default',
'Configure providers from the Dashboard or API',
],
cta: { label: 'Learn more', href: '/features/custom-oidc-providers' },
},
]
export function HighlightsSection() {
return (
<div className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Enterprise ready
<br />
<span className="text-foreground">security at every level</span>
</h3>
</div>
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{HIGHLIGHTS.map((highlight) => (
<div
key={highlight.title}
className="relative flex flex-col overflow-hidden min-h-[400px] bg-surface-75 border border-border rounded-lg"
>
<div className="relative z-10 flex flex-col justify-between h-full p-6 md:p-8">
<div className="flex flex-col gap-4">
<h4 className="text-foreground text-lg font-medium">{highlight.title}</h4>
<ul className="flex flex-col text-foreground-lighter text-sm gap-1.5">
{highlight.features.map((feature) => (
<li key={feature} className="flex items-start gap-2">
<Check className="w-3.5 h-3.5 mt-0.5 shrink-0 stroke-2 text-brand" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
<div className="mt-6">
<Button type="default" size="small" asChild>
<Link href={highlight.cta.href}>{highlight.cta.label}</Link>
</Button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
@@ -0,0 +1,16 @@
import { QuoteSection as SharedQuoteSection } from '../../_shared/QuoteSection'
export function QuoteSection() {
return (
<SharedQuoteSection
quote="Supabase is not only super easy to get started, but also provides all the backend solutions we require as we continue to grow."
highlight="The auth system just works out of the box."
author={{
name: 'Alfred Lua',
role: 'Cofounder of Pebblely',
image: '/images/blog/avatars/alfred-lua-pebblely.jpeg',
link: '/customers/pebblely',
}}
/>
)
}
@@ -0,0 +1,129 @@
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { RLSSectionClient } from './RLSSectionClient'
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#bda4ff' },
},
{ scope: ['entity.name.function', 'support.function'], settings: { foreground: '#3ecf8e' } },
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#3ecf8e' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
],
}
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#6b35dc' },
},
{ scope: ['entity.name.function', 'support.function'], settings: { foreground: '#15593b' } },
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#15593b' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
],
}
const RLS_EXAMPLES = [
{
title: 'Allow read access',
description: 'Public profiles are viewable by everyone.',
code: `-- 1. Create table
create table profiles (
id serial primary key,
name text
);
-- 2. Enable RLS
alter table profiles enable row level security;
-- 3. Create Policy
create policy "Public profiles are viewable by everyone."
on profiles for select
using ( true );`,
},
{
title: 'Restrict updates',
description: 'Users can only update their own rows.',
code: `-- 1. Create table
create table profiles (
id serial primary key,
name text
);
-- 2. Enable RLS
alter table profiles enable row level security;
-- 3. Create Policy
create policy "Users can update their own profiles."
on profiles for update
using ( (select auth.uid()) = id );`,
},
{
title: 'Advanced rules',
description: 'Team members can update team details using joins.',
code: `create table teams (
id bigint primary key generated always as identity,
name text
);
create table members (
team_id bigint references teams,
user_id uuid references auth.users
);
alter table teams enable row level security;
create policy "Team members can update team details if they belong to the team"
on teams
for update using (
(select auth.uid()) in (
select user_id from members
where team_id = id
)
);`,
},
]
export async function RLSSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: ['sql'],
})
const examples = RLS_EXAMPLES.map((example) => ({
title: example.title,
description: example.description,
darkHtml: hl.codeToHtml(example.code, { lang: 'sql', theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(example.code, { lang: 'sql', theme: 'supabase-light' }),
}))
hl.dispose()
return <RLSSectionClient examples={examples} />
}
@@ -0,0 +1,200 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
type RLSExample = {
title: string
description: string
darkHtml: string
lightHtml: string
}
const INTERVAL_DURATION = 10000
export function RLSSectionClient({ examples }: { examples: RLSExample[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = examples[activeIdx]
// Auto-size the code panel to the active snippet. We measure an always-mounted,
// invisible copy of the current code so the target height is known immediately on
// switch (and doesn't collapse during the cross-fade), then animate `height` to it.
const measureRef = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState<number>()
const hasMeasured = useRef(false)
useEffect(() => {
const el = measureRef.current
if (!el) return
const ro = new ResizeObserver(() => setHeight(el.offsetHeight))
ro.observe(el)
setHeight(el.offsetHeight)
return () => ro.disconnect()
}, [])
useEffect(() => {
if (height != null) hasMeasured.current = true
}, [height])
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % examples.length)
}
}, [progress, examples.length])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
User permissions
<br />
<span className="text-foreground">without the middleware</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Build authorization rules with Postgres Row Level Security — control who can create,
edit, and delete specific rows in your database. No additional servers required.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tabs */}
<div className="flex flex-col gap-3 items-start justify-end">
{examples.map((example, index) => {
const isActive = index === activeIdx
return (
<button
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex flex-col gap-0.5 py-2 transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<span className="text-2xl font-medium">{example.title}</span>
<span
className={cn(
'text-sm transition-colors',
isActive ? 'text-foreground-lighter' : 'text-foreground-muted'
)}
>
{example.description}
</span>
</button>
)
})}
</div>
{/* Right: code block */}
<motion.div
initial={false}
animate={height != null ? { height } : undefined}
transition={
hasMeasured.current ? { duration: 0.35, ease: [0.22, 1, 0.36, 1] } : { duration: 0 }
}
className="relative self-end overflow-hidden border border-border rounded-md"
>
{/* Invisible measuring copy: drives the animated container height */}
<div
ref={measureRef}
aria-hidden
className="pointer-events-none invisible absolute inset-x-0 top-0 overflow-x-auto pb-12 [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
/>
<AnimatePresence mode="wait">
<motion.div
key={active.title}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="overflow-x-auto pb-12"
>
<div
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
className="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
<div
dangerouslySetInnerHTML={{ __html: active.lightHtml }}
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
<Link
href="/docs/guides/database/postgres/row-level-security"
className="absolute bottom-4 right-4 flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap"
>
Documentation
<svg
width={12}
height={12}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0"
>
<path
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</motion.div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,226 @@
'use client'
import { useEffect, useRef } from 'react'
// ─── SVG paths (shield-user) ────────────────────────────────────────────────
const SHIELD_PATH =
'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z'
// ─── Shaders ─────────────────────────────────────────────────────────────────
const vert = /* glsl */ `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`
const frag = /* glsl */ `
precision highp float;
uniform sampler2D tMap;
uniform float uHover;
uniform float uTime;
uniform vec2 uResolution;
varying vec2 vUv;
#define GREEN vec3(0.243, 0.812, 0.557)
// ── Noise ──
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),
f.y
);
}
float fbm(vec2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * noise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
// ── Bayer dithering (recursive 8×8) ──
float bayer2(vec2 a) {
a = floor(a);
return fract(a.x / 2.0 + a.y * a.y * 0.75);
}
float bayer4(vec2 a) { return bayer2(0.5 * a) * 0.25 + bayer2(a); }
float bayer8(vec2 a) { return bayer4(0.5 * a) * 0.25 + bayer2(a); }
void main() {
float shape = texture2D(tMap, vUv).r;
if (shape < 0.5) { gl_FragColor = vec4(0.0); return; }
// Animated FBM noise — continuous drift
vec2 noiseUv = vUv * 3.0 + vec2(uTime * 0.8, uTime * 0.5);
float n = fbm(noiseUv);
float value = n * 0.7 + 0.3;
// Bayer dithering — divide resolution for bigger dots
vec2 screenPos = vUv * uResolution / 3.0;
float dither = bayer8(screenPos);
float dithered = smoothstep(dither - 0.04, dither + 0.04, value);
vec3 col = GREEN * dithered;
float alpha = dithered;
gl_FragColor = vec4(col * alpha, alpha);
}
`
// ─── Helpers ─────────────────────────────────────────────────────────────────
function buildSVGCanvas(size: number): Promise<HTMLCanvasElement> {
return new Promise((resolve) => {
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const ctx = canvas.getContext('2d')!
const svgSrc = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="${size}" height="${size}">
<path d="${SHIELD_PATH}" fill="white"/>
</svg>`
const blob = new Blob([svgSrc], { type: 'image/svg+xml;charset=utf-8' })
const url = URL.createObjectURL(blob)
const img = new Image()
img.onload = () => {
ctx.drawImage(img, 0, 0)
URL.revokeObjectURL(url)
resolve(canvas)
}
img.src = url
})
}
// ─── Component ───────────────────────────────────────────────────────────────
interface Props {
hovered: boolean
}
export function ShieldGlow({ hovered }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const liveRef = useRef({ hovered: false, hover: 0 })
useEffect(() => {
liveRef.current.hovered = hovered
}, [hovered])
useEffect(() => {
const canvasMaybe = canvasRef.current
if (!canvasMaybe) return
const canvas: HTMLCanvasElement = canvasMaybe
let running = true
let raf: number
async function init() {
const { Renderer, Program, Mesh, Triangle, Texture } = await import('ogl')
if (!running) return
const svgCanvas = await buildSVGCanvas(512)
if (!running) return
const renderer = new Renderer({
canvas,
alpha: true,
premultipliedAlpha: true,
dpr: window.devicePixelRatio,
})
const gl = renderer.gl
gl.clearColor(0, 0, 0, 0)
const texture = new Texture(gl, {
image: svgCanvas,
generateMipmaps: false,
minFilter: gl.LINEAR,
magFilter: gl.LINEAR,
})
const geometry = new Triangle(gl)
const program = new Program(gl, {
vertex: vert,
fragment: frag,
uniforms: {
tMap: { value: texture },
uHover: { value: 0 },
uTime: { value: 0 },
uResolution: { value: [canvas.width, canvas.height] },
},
transparent: true,
depthTest: false,
depthWrite: false,
})
const mesh = new Mesh(gl, { geometry, program })
const container = canvas.parentElement!
function resize(entries?: ResizeObserverEntry[]) {
const w = entries?.[0]?.contentRect.width ?? container.offsetWidth
const h = entries?.[0]?.contentRect.height ?? container.offsetHeight
renderer.setSize(w, h)
program.uniforms.uResolution.value = [canvas.width, canvas.height]
}
resize()
const ro = new ResizeObserver(resize)
ro.observe(container)
function loop() {
if (!running) return
raf = requestAnimationFrame(loop)
const live = liveRef.current
const target = live.hovered ? 1 : 0
live.hover += (target - live.hover) * 0.06
program.uniforms.uHover.value = live.hover
program.uniforms.uTime.value = performance.now() / 1000
renderer.render({ scene: mesh })
}
loop()
return () => ro.disconnect()
}
let cleanupResize: (() => void) | undefined
init().then((c) => {
cleanupResize = c
})
return () => {
running = false
cancelAnimationFrame(raf)
cleanupResize?.()
}
}, [])
return (
<canvas
ref={canvasRef}
width={320}
height={320}
className="pointer-events-none absolute inset-0 h-full w-full"
style={{ mixBlendMode: 'screen' }}
/>
)
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from 'next'
import { ApiSection } from './_components/ApiSection'
import { AuthContent } from './_components/AuthContent'
import { RLSSection } from './_components/RLSSection'
export const metadata: Metadata = {
title: 'Auth | Supabase',
description:
'Add user sign ups and logins to your app. Secure your data with Row Level Security. Works with all major OAuth providers.',
}
export default function AuthPage() {
return <AuthContent apiSlot={<ApiSection />} rlsSlot={<RLSSection />} />
}
@@ -0,0 +1,412 @@
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { ApiSectionClient } from './ApiSectionClient'
// ── Shiki themes ────────────────────────────────────────────────────────────
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#bda4ff' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#3ecf8e' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#3ecf8e' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#3ecf8e' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#ffffff' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
{ scope: ['markup.underline.link'], settings: { foreground: '#ffffff' } },
{ scope: ['markup.inserted'], settings: { foreground: '#3ecf8e' } },
{ scope: ['markup.deleted'], settings: { foreground: '#F06A50' } },
],
}
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#6b35dc' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#15593b' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#15593b' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#15593b' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#525252' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
{ scope: ['markup.underline.link'], settings: { foreground: '#525252' } },
],
}
// ── Language → Shiki lang mapping ───────────────────────────────────────────
const LANG_MAP: Record<string, string> = {
JavaScript: 'javascript',
Flutter: 'dart',
Python: 'python',
'C#': 'csharp',
Kotlin: 'kotlin',
Swift: 'swift',
}
// ── API examples per language ───────────────────────────────────────────────
const API_EXAMPLES = [
{
icon: 'Search' as const,
title: 'Fetch records',
description: 'Query tables with joins, filters, and ordering in a single request.',
code: {
JavaScript: `// Fetch all public rooms with their messages
const { data: rooms } = await supabase
.from('rooms')
.select(\`
id, name, created_at,
messages ( id, text, user_id )
\`)
.eq('public', true)
.order('created_at', { ascending: false })
.limit(20)`,
Flutter: `// Fetch all public rooms with their messages
final rooms = await supabase
.from('rooms')
.select('id, name, created_at, messages ( id, text, user_id )')
.eq('public', true)
.order('created_at', ascending: false)
.limit(20);`,
Python: `# Fetch all public rooms with their messages
rooms = supabase.table("rooms") \\
.select("id, name, created_at, messages ( id, text, user_id )") \\
.eq("public", True) \\
.order("created_at", desc=True) \\
.limit(20) \\
.execute()`,
'C#': `// Fetch all public rooms with their messages
var rooms = await supabase
.From<Room>()
.Select("id, name, created_at, messages ( id, text, user_id )")
.Filter("public", Operator.Equals, "true")
.Order("created_at", Ordering.Descending)
.Limit(20)
.Get();`,
Kotlin: `// Fetch all public rooms with their messages
val rooms = supabase.from("rooms")
.select(Columns.raw("""
id, name, created_at,
messages ( id, text, user_id )
""")) {
filter { eq("public", true) }
order("created_at", Order.DESCENDING)
limit(20)
}`,
Swift: `// Fetch all public rooms with their messages
let rooms: [Room] = try await supabase
.from("rooms")
.select("id, name, created_at, messages ( id, text, user_id )")
.eq("public", value: true)
.order("created_at", ascending: false)
.limit(20)
.execute()
.value`,
},
},
{
icon: 'Plus' as const,
title: 'Insert with relations',
description: 'Create rows across related tables with automatic foreign key resolution.',
code: {
JavaScript: `// Create a room and its first message
const { data: room } = await supabase
.from('rooms')
.insert({ name: 'Design Team', public: false })
.select()
.single()
await supabase
.from('messages')
.insert({
room_id: room.id,
text: 'Welcome to the team channel!',
user_id: session.user.id,
})`,
Flutter: `// Create a room and its first message
final room = await supabase
.from('rooms')
.insert({'name': 'Design Team', 'public': false})
.select()
.single();
await supabase
.from('messages')
.insert({
'room_id': room['id'],
'text': 'Welcome to the team channel!',
'user_id': session.user.id,
});`,
Python: `# Create a room and its first message
room = supabase.table("rooms") \\
.insert({"name": "Design Team", "public": False}) \\
.execute()
supabase.table("messages") \\
.insert({
"room_id": room.data[0]["id"],
"text": "Welcome to the team channel!",
"user_id": session.user.id,
}) \\
.execute()`,
'C#': `// Create a room and its first message
var room = await supabase
.From<Room>()
.Insert(new Room { Name = "Design Team", Public = false })
.Single();
await supabase
.From<Message>()
.Insert(new Message {
RoomId = room.Id,
Text = "Welcome to the team channel!",
UserId = session.User.Id,
});`,
Kotlin: `// Create a room and its first message
val room = supabase.from("rooms")
.insert(Room(name = "Design Team", public = false)) {
select()
}.decodeSingle<Room>()
supabase.from("messages")
.insert(Message(
roomId = room.id,
text = "Welcome to the team channel!",
userId = session.user.id,
))`,
Swift: `// Create a room and its first message
let room: Room = try await supabase
.from("rooms")
.insert(["name": "Design Team", "public": false])
.select()
.single()
.execute()
.value
try await supabase
.from("messages")
.insert([
"room_id": room.id,
"text": "Welcome to the team channel!",
"user_id": session.user.id,
])
.execute()`,
},
},
{
icon: 'ArrowDownUp' as const,
title: 'Update records',
description: 'Modify rows with conditions and return the updated data in one call.',
code: {
JavaScript: `// Update a room and return the updated row
const { data: updated } = await supabase
.from('rooms')
.update({
name: 'Engineering Team',
public: false,
updated_at: new Date().toISOString(),
})
.eq('id', roomId)
.select()
.single()`,
Flutter: `// Update a room and return the updated row
final updated = await supabase
.from('rooms')
.update({
'name': 'Engineering Team',
'public': false,
'updated_at': DateTime.now().toIso8601String(),
})
.eq('id', roomId)
.select()
.single();`,
Python: `# Update a room and return the updated row
updated = supabase.table("rooms") \\
.update({
"name": "Engineering Team",
"public": False,
"updated_at": datetime.now().isoformat(),
}) \\
.eq("id", room_id) \\
.execute()`,
'C#': `// Update a room and return the updated row
var updated = await supabase
.From<Room>()
.Filter("id", Operator.Equals, roomId)
.Set(x => x.Name, "Engineering Team")
.Set(x => x.Public, false)
.Set(x => x.UpdatedAt, DateTime.UtcNow)
.Update()
.Single();`,
Kotlin: `// Update a room and return the updated row
val updated = supabase.from("rooms")
.update({
Room::name setTo "Engineering Team"
Room::public setTo false
Room::updatedAt setTo Clock.System.now()
}) {
filter { eq("id", roomId) }
select()
}.decodeSingle<Room>()`,
Swift: `// Update a room and return the updated row
let updated: Room = try await supabase
.from("rooms")
.update([
"name": "Engineering Team",
"public": false,
"updated_at": ISO8601DateFormatter().string(from: Date()),
])
.eq("id", value: roomId)
.select()
.single()
.execute()
.value`,
},
},
{
icon: 'Filter' as const,
title: 'Filter & paginate',
description: 'Full-text search, range queries, and cursor-based pagination built in.',
code: {
JavaScript: `// Full-text search with pagination
const { data, count } = await supabase
.from('rooms')
.select('id, name, member_count', { count: 'exact' })
.textSearch('name', 'design')
.gte('member_count', 5)
.range(0, 9)
.order('member_count', { ascending: false })`,
Flutter: `// Full-text search with pagination
final response = await supabase
.from('rooms')
.select('id, name, member_count', const FetchOptions(count: CountOption.exact))
.textSearch('name', 'design')
.gte('member_count', 5)
.range(0, 9)
.order('member_count', ascending: false);`,
Python: `# Full-text search with pagination
response = supabase.table("rooms") \\
.select("id, name, member_count", count="exact") \\
.text_search("name", "design") \\
.gte("member_count", 5) \\
.range(0, 9) \\
.order("member_count", desc=True) \\
.execute()`,
'C#': `// Full-text search with pagination
var response = await supabase
.From<Room>()
.Select("id, name, member_count")
.Filter("name", Operator.FTS, "design")
.Filter("member_count", Operator.GreaterThanOrEqual, "5")
.Range(0, 9)
.Order("member_count", Ordering.Descending)
.Get();`,
Kotlin: `// Full-text search with pagination
val response = supabase.from("rooms")
.select(Columns.list("id", "name", "member_count")) {
filter {
textSearch("name", "design")
gte("member_count", 5)
}
range(0L..9L)
order("member_count", Order.DESCENDING)
}`,
Swift: `// Full-text search with pagination
let response = try await supabase
.from("rooms")
.select("id, name, member_count", head: false, count: .exact)
.textSearch("name", query: "design")
.gte("member_count", value: 5)
.range(from: 0, to: 9)
.order("member_count", ascending: false)
.execute()`,
},
},
]
// ── Server component: pre-highlights all code ───────────────────────────────
export async function ApiSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: Object.values(LANG_MAP),
})
const examples = API_EXAMPLES.map((example) => ({
icon: example.icon,
title: example.title,
description: example.description,
languages: Object.fromEntries(
Object.entries(example.code).map(([lang, code]) => [
lang,
{
darkHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-light' }),
},
])
),
}))
hl.dispose()
return <ApiSectionClient examples={examples} />
}
@@ -0,0 +1,222 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { ArrowDownUp, ChevronDown, Filter, Plus, Search } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { Search, Plus, ArrowDownUp, Filter } as const
type IconName = keyof typeof ICONS
type ApiExample = {
icon: IconName
title: string
description: string
languages: Record<string, { darkHtml: string; lightHtml: string }>
}
const LANGUAGES = ['JavaScript', 'Flutter', 'Python', 'C#', 'Kotlin', 'Swift']
const INTERVAL_DURATION = 8000
export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [activeLang, setActiveLang] = useState('JavaScript')
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = examples[activeIdx]
// Auto-size the code panel to the active snippet. We measure an always-mounted,
// invisible copy of the current code so the target height is known immediately on
// switch (and doesn't collapse during the cross-fade), then animate `height` to it.
const measureRef = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState<number>()
const hasMeasured = useRef(false)
useEffect(() => {
const el = measureRef.current
if (!el) return
const ro = new ResizeObserver(() => setHeight(el.offsetHeight))
ro.observe(el)
setHeight(el.offsetHeight)
return () => ro.disconnect()
}, [])
useEffect(() => {
if (height != null) hasMeasured.current = true
}, [height])
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % examples.length)
}
}, [progress, examples.length])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-24">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Never write
<br />
<span className="text-foreground">an API again</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
We introspect your database and provide instant APIs. Focus on building your product,
while Supabase handles the CRUD.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tab cards */}
<div className="flex flex-col gap-1 items-start justify-end">
{examples.map((example, index) => {
const isActive = index === activeIdx
const Icon = ICONS[example.icon]
return (
<button
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{example.title}
</button>
)
})}
</div>
{/* Right: code block */}
<motion.div
initial={false}
animate={height != null ? { height } : undefined}
transition={
hasMeasured.current ? { duration: 0.35, ease: [0.22, 1, 0.36, 1] } : { duration: 0 }
}
className="relative self-end overflow-hidden border border-border rounded-md"
>
{/* Invisible measuring copy: drives the animated container height */}
<div
ref={measureRef}
aria-hidden
className="pointer-events-none invisible absolute inset-x-0 top-0 overflow-x-auto pb-12 [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: active.languages[activeLang]?.darkHtml ?? '' }}
/>
{/* Language switcher */}
<div className="absolute top-3 right-4 z-10 flex items-center">
<div className="relative flex items-center gap-1 text-foreground-muted text-xs">
<select
value={activeLang}
onChange={(e) => setActiveLang(e.target.value)}
className="appearance-none bg-transparent text-foreground-light text-xs pr-4 cursor-pointer border-none outline-none shadow-none focus:border-none focus:outline-none focus:shadow-none focus:ring-0"
>
{LANGUAGES.map((lang) => (
<option key={lang} value={lang}>
{lang}
</option>
))}
</select>
<ChevronDown size={12} className="absolute right-0 pointer-events-none" />
</div>
</div>
<AnimatePresence mode="wait">
<motion.div
key={`${active.title}-${activeLang}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="overflow-x-auto pb-12"
>
<div
dangerouslySetInnerHTML={{ __html: active.languages[activeLang]?.darkHtml ?? '' }}
className="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
<div
dangerouslySetInnerHTML={{
__html: active.languages[activeLang]?.lightHtml ?? '',
}}
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
<Link
href="/docs/guides/database"
className="absolute bottom-4 right-4 flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap"
>
Documentation
<svg
width={12}
height={12}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0"
>
<path
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</motion.div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,42 @@
import { BuiltWithSupabaseSection } from '~/app/(home)/_components/BuiltWithSupabaseSection'
import { CTASection } from '~/app/(home)/_components/CTASection'
import { FeaturesSection } from './FeaturesSection'
import { Hero } from './Hero'
import { HighlightsSection } from './HighlightsSection'
import { QuoteSection } from './QuoteSection'
import { SqlEditorSection } from './SqlEditorSection'
import { TableEditorSection } from './TableEditorSection'
import { VisualDemo } from './VisualDemo'
export function DatabaseContent({ apiSlot }: { apiSlot: React.ReactNode }) {
return (
<div className="overflow-x-clip">
<section>
<Hero />
<VisualDemo />
</section>
<section className="border-t border-border">
<FeaturesSection />
</section>
<section className="border-t border-border">
<QuoteSection />
</section>
<section className="border-t border-border">
<HighlightsSection />
</section>
<section className="border-t border-border">
<TableEditorSection />
</section>
<section className="border-t border-border">
<SqlEditorSection />
</section>
<section className="border-t border-border">{apiSlot}</section>
<section className="border-t border-border">
<BuiltWithSupabaseSection />
</section>
<section className="border-t border-border">
<CTASection />
</section>
</div>
)
}
@@ -0,0 +1,605 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { Key } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { cn } from 'ui'
import { PostgresGlow } from './Shaders/PostgresGlow'
function SkeletonBar({ className }: { className?: string }) {
return <div className={cn('rounded bg-foreground-muted/10', className)} />
}
const FEATURES = [
{
title: 'Just Postgres',
description: 'A dedicated Postgres database.',
detail: '100% portable. Bring your existing Postgres database, or migrate away at any time.',
visual: PostgresSkeleton,
},
{
title: 'Secure by default',
description: "Built on Postgres' proven Row Level Security.",
detail: 'Integrated with JWT authentication which controls exactly what your users can access.',
visual: RLSSkeleton,
},
{
title: 'Realtime enabled',
description: 'Data-change listeners over websockets.',
detail: 'Subscribe and react to database changes, milliseconds after they happen.',
visual: RealtimeSkeleton,
},
]
function PostgresSkeleton() {
const [hovered, setHovered] = useState(false)
return (
<div
className="flex items-center justify-center w-full h-full relative"
style={{
maskImage: 'radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 75%)',
WebkitMaskImage: 'radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 75%)',
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{/* Abstract construction lines behind the elephant */}
<svg
className="absolute -inset-16 w-[calc(100%+128px)] h-[calc(100%+128px)] text-foreground-muted/20"
viewBox="0 0 480 480"
fill="none"
>
<rect
x="60"
y="40"
width="360"
height="400"
rx="48"
stroke="currentColor"
strokeWidth="0.5"
/>
<rect
x="30"
y="10"
width="420"
height="460"
rx="60"
stroke="currentColor"
strokeWidth="0.5"
/>
<rect
x="0"
y="-16"
width="480"
height="512"
rx="72"
stroke="currentColor"
strokeWidth="0.5"
/>
<rect
x="90"
y="70"
width="300"
height="340"
rx="36"
stroke="currentColor"
strokeWidth="0.5"
/>
<line x1="30" y1="10" x2="450" y2="470" stroke="currentColor" strokeWidth="0.5" />
<line x1="450" y1="10" x2="30" y2="470" stroke="currentColor" strokeWidth="0.5" />
<line x1="240" y1="0" x2="240" y2="480" stroke="currentColor" strokeWidth="0.5" />
<line x1="0" y1="240" x2="480" y2="240" stroke="currentColor" strokeWidth="0.5" />
<circle cx="240" cy="240" r="180" stroke="currentColor" strokeWidth="0.5" />
<circle cx="240" cy="240" r="140" stroke="currentColor" strokeWidth="0.5" />
<circle cx="240" cy="240" r="100" stroke="currentColor" strokeWidth="0.5" />
<line x1="60" y1="40" x2="60" y2="10" stroke="currentColor" strokeWidth="0.5" />
<line x1="60" y1="40" x2="30" y2="40" stroke="currentColor" strokeWidth="0.5" />
<line x1="420" y1="40" x2="450" y2="40" stroke="currentColor" strokeWidth="0.5" />
<line x1="420" y1="40" x2="420" y2="10" stroke="currentColor" strokeWidth="0.5" />
<line x1="60" y1="440" x2="30" y2="440" stroke="currentColor" strokeWidth="0.5" />
<line x1="60" y1="440" x2="60" y2="470" stroke="currentColor" strokeWidth="0.5" />
<line x1="420" y1="440" x2="450" y2="440" stroke="currentColor" strokeWidth="0.5" />
<line x1="420" y1="440" x2="420" y2="470" stroke="currentColor" strokeWidth="0.5" />
</svg>
{/* Highlighted lines on hover */}
<svg
className="absolute -inset-16 w-[calc(100%+128px)] h-[calc(100%+128px)] transition-opacity duration-500"
style={{ opacity: hovered ? 0.35 : 0 }}
viewBox="0 0 480 480"
fill="none"
>
<rect
x="60"
y="40"
width="360"
height="400"
rx="48"
stroke="hsl(var(--brand-default))"
strokeWidth="0.75"
opacity="0.4"
/>
<circle
cx="240"
cy="240"
r="140"
stroke="hsl(var(--brand-default))"
strokeWidth="0.75"
opacity="0.3"
/>
<line
x1="240"
y1="0"
x2="240"
y2="480"
stroke="hsl(var(--brand-default))"
strokeWidth="0.75"
opacity="0.25"
/>
<line
x1="0"
y1="240"
x2="480"
y2="240"
stroke="hsl(var(--brand-default))"
strokeWidth="0.75"
opacity="0.25"
/>
</svg>
<div className="relative w-80 h-80">
<PostgresGlow hovered={hovered} />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 390 430"
className="w-80 h-80 relative z-[5]"
style={{
color: hovered ? 'hsl(var(--brand-default))' : 'hsl(var(--foreground-muted))',
opacity: hovered ? 1 : 0.8,
transition: 'color 0.5s, opacity 0.5s',
}}
>
<path
stroke="currentColor"
strokeWidth="1.5"
d="M192.144 125.816h-53.465c-8.506 0-16.159 5.17-19.334 13.061L99.0045 189.43c-3.0613 7.608-1.3448 16.306 4.3775 22.181l10.232 10.506c4.792 4.919 7.474 11.516 7.474 18.384l-.001 14.473c0 20.197 16.373 36.569 36.569 36.569 6.16 0 11.154-4.993 11.154-11.153l.001-86.241c0-18.629 7.441-36.486 20.668-49.602 2.746-2.723 7.178-2.704 9.9.041 2.722 2.745 2.703 7.178-.042 9.9-10.577 10.488-16.526 24.766-16.526 39.661l-.001 86.241c0 13.892-11.262 25.153-25.154 25.153-27.928 0-50.569-22.64-50.569-50.569l.001-14.474c0-3.218-1.257-6.309-3.503-8.615L93.353 221.38c-9.5904-9.847-12.4673-24.424-7.3366-37.176l20.3406-50.553c5.308-13.192 18.101-21.835 32.322-21.835h55.729v.084h10.339c49.104 0 88.91 39.806 88.91 88.91v50.842c0 3.866-3.134 7-7 7s-7-3.134-7-7V200.81c0-41.372-33.538-74.91-74.91-74.91H193.23c-.37 0-.732-.029-1.086-.084Z"
/>
<path
stroke="currentColor"
strokeWidth="1.5"
d="M210.03 283.94c0-3.866-3.134-7-7-7s-7 3.134-7 7v3.113c0 26.959 21.854 48.814 48.813 48.814 26.351 0 47.825-20.879 48.781-46.996h24.614c3.866 0 7-3.134 7-7s-3.134-7-7-7h-26.841c-30.744 0-60.256-12.083-82.173-33.643-2.756-2.711-7.188-2.675-9.899.081-2.711 2.756-2.675 7.188.081 9.9 21.725 21.371 50.116 34.423 80.228 37.134-.679 18.629-15.995 33.524-34.791 33.524-19.227 0-34.813-15.587-34.813-34.814v-3.113Z"
/>
<path
stroke="currentColor"
strokeWidth="1.5"
d="M238.03 202.145c0 4.792 3.885 8.677 8.677 8.677s8.676-3.885 8.676-8.677-3.884-8.676-8.676-8.676-8.677 3.884-8.677 8.676Z"
/>
</svg>
</div>
</div>
)
}
const rlsPolicies = [
{ name: 'Users can read their own profile', op: 'SELECT', clause: 'auth.uid() = id' },
{ name: 'Users can update their own data', op: 'UPDATE', clause: 'auth.uid() = id' },
{ name: 'Users can delete their own posts', op: 'DELETE', clause: 'auth.uid() = id' },
]
const rlsColumns = [
{ name: 'id', format: 'int8', isPrimaryKey: true },
{ name: 'email', format: 'text' },
{ name: 'role', format: 'text' },
]
const rlsRows = [
{ id: '1', email: 'alice@company.com', role: 'admin' },
{ id: '2', email: 'bob@company.com', role: 'user' },
]
function RLSSkeleton() {
const [policyIdx, setPolicyIdx] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
const [isInView, setIsInView] = useState(false)
useEffect(() => {
const el = containerRef.current
if (!el) return
const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), {
threshold: 0.3,
})
observer.observe(el)
return () => observer.disconnect()
}, [])
useEffect(() => {
if (!isInView) return
const timer = setInterval(() => setPolicyIdx((i) => (i + 1) % rlsPolicies.length), 10000)
return () => clearInterval(timer)
}, [isInView])
const policy = rlsPolicies[policyIdx]
return (
<div ref={containerRef} className="relative w-full h-full flex flex-col justify-end">
<div
className="absolute top-12 -right-24"
style={{
maskImage: 'linear-gradient(to right, black 35%, transparent 70%)',
WebkitMaskImage: 'linear-gradient(to right, black 35%, transparent 70%)',
}}
>
<div className="border border-border rounded-lg overflow-hidden" style={{ maxHeight: 180 }}>
<table className="w-full border-collapse text-[13px] !mt-0" style={{ minWidth: 360 }}>
<thead>
<tr className="bg-surface-200">
{rlsColumns.map((col) => (
<th
key={col.name}
className="border-b border-r last:border-r-0 border-default px-3 py-1.5 text-left font-normal"
>
<div className="flex items-center gap-1.5 overflow-hidden">
{col.isPrimaryKey && (
<Key size={12} strokeWidth={2} className="text-brand rotate-45 shrink-0" />
)}
<span className="text-foreground text-xs truncate font-medium">
{col.name}
</span>
<span className="text-foreground-light text-xs truncate">{col.format}</span>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{rlsRows.map((row) => (
<tr key={row.id} className="border-b border-default last:border-b-0 bg-surface-75">
<td className="border-r border-default px-3 py-1.5 text-foreground-muted text-xs">
{row.id}
</td>
<td className="border-r border-default px-3 py-1.5 text-foreground text-xs">
{row.email}
</td>
<td className="px-3 py-1.5 text-foreground-muted text-xs">{row.role}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* SQL snippet — bottom left */}
<div className="px-6 pb-4">
<pre className="text-sm leading-relaxed font-mono text-[#525252] dark:text-white overflow-hidden">
<span className="text-[#6b35dc] dark:text-[#bda4ff]">CREATE POLICY</span>
{'\n'}
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={policy.name}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
className="inline-block text-[#f1a10d] dark:text-[#ffcda1]"
>
&quot;{policy.name}&quot;
</motion.span>
</AnimatePresence>
{'\n'}
<span className="text-[#6b35dc] dark:text-[#bda4ff]">ON</span>
{' public.users\n'}
<span className="text-[#6b35dc] dark:text-[#bda4ff]">FOR </span>
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={policy.op}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
className="inline-block text-[#6b35dc] dark:text-[#bda4ff]"
>
{policy.op}
</motion.span>
</AnimatePresence>{' '}
<span className="text-[#6b35dc] dark:text-[#bda4ff]">USING</span>
{'\n ('}
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={policy.clause}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2 }}
className="inline-block"
>
{policy.clause}
</motion.span>
</AnimatePresence>
{');'}
</pre>
</div>
</div>
)
}
type ChatMsg = { id: number; user: string; text: string }
const initialMessages: ChatMsg[] = [
{ id: 1, user: 'Alice', text: 'Hey, is the deploy ready?' },
{ id: 2, user: 'Bob', text: 'Almost — running final tests now.' },
{ id: 3, user: 'Alice', text: 'Nice, let me know when it\u2019s live.' },
]
const incomingMessages: ChatMsg[] = [
{ id: 4, user: 'Bob', text: 'All green. Deploying now 🚀' },
{ id: 5, user: 'Alice', text: 'Awesome, checking it out.' },
{ id: 6, user: 'Bob', text: 'Latency dropped to 42ms!' },
{ id: 7, user: 'Alice', text: 'That\u2019s a big improvement.' },
{ id: 8, user: 'Bob', text: 'Yeah, the new index helped.' },
{ id: 9, user: 'Alice', text: 'Should we update the docs?' },
{ id: 10, user: 'Bob', text: 'Already on it.' },
]
const chatCols = ['id', 'user', 'text'] as const
function RealtimeSkeleton() {
const [messages, setMessages] = useState<ChatMsg[]>(initialMessages)
const [tableFlashId, setTableFlashId] = useState<number | null>(null)
const [chatFlashId, setChatFlashId] = useState<number | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const tableScrollRef = useRef<HTMLDivElement>(null)
const chatScrollRef = useRef<HTMLDivElement>(null)
const [isInView, setIsInView] = useState(false)
const nextMsgIdx = useRef(0)
useEffect(() => {
const el = containerRef.current
if (!el) return
const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), {
threshold: 0.3,
})
observer.observe(el)
return () => observer.disconnect()
}, [])
useEffect(() => {
if (!isInView) return
const timer = setInterval(() => {
const msg = incomingMessages[nextMsgIdx.current % incomingMessages.length]
// Give each added message a unique id
const newMsg = { ...msg, id: Date.now() + nextMsgIdx.current }
nextMsgIdx.current++
// Add to table + flash
setMessages((prev) => [...prev, newMsg])
setTableFlashId(newMsg.id)
// Autoscroll table
requestAnimationFrame(() => {
tableScrollRef.current?.scrollTo({
top: tableScrollRef.current.scrollHeight,
behavior: 'smooth',
})
})
// Chat updates after short delay (realtime propagation)
setTimeout(() => {
setChatFlashId(newMsg.id)
requestAnimationFrame(() => {
chatScrollRef.current?.scrollTo({
top: chatScrollRef.current.scrollHeight,
behavior: 'smooth',
})
})
setTimeout(() => {
setTableFlashId(null)
setChatFlashId(null)
}, 600)
}, 200)
}, 3000)
return () => clearInterval(timer)
}, [isInView])
return (
<div
ref={containerRef}
className="relative w-full h-full flex flex-col justify-end"
style={{
maskImage:
'linear-gradient(to bottom, black 50%, rgba(0,0,0,0.9) 65%, rgba(0,0,0,0.5) 75%, rgba(0,0,0,0.1) 85%, transparent 90%)',
WebkitMaskImage:
'linear-gradient(to bottom, black 50%, rgba(0,0,0,0.9) 65%, rgba(0,0,0,0.5) 75%, rgba(0,0,0,0.1) 85%, transparent 90%)',
}}
>
<div
className="absolute top-12 -right-24 z-[2]"
style={{
maskImage: 'linear-gradient(to right, black 35%, transparent 70%)',
WebkitMaskImage: 'linear-gradient(to right, black 35%, transparent 70%)',
}}
>
<div
className="border border-border rounded-lg overflow-hidden bg-surface-75"
style={{ width: 420 }}
>
<table className="w-full border-collapse text-[13px] !mt-0 table-fixed">
<colgroup>
<col style={{ width: 72 }} />
<col style={{ width: 100 }} />
<col />
</colgroup>
<thead>
<tr className="bg-surface-200">
<th className="border-b border-r border-default px-3 py-1.5 text-left font-normal">
<div className="flex items-center gap-1.5 overflow-hidden">
<Key size={12} strokeWidth={2} className="text-brand rotate-45 shrink-0" />
<span className="text-foreground text-xs font-medium">id</span>
<span className="text-foreground-light text-xs">int8</span>
</div>
</th>
<th className="border-b border-r border-default px-3 py-1.5 text-left font-normal">
<div className="flex items-center gap-1.5 overflow-hidden">
<span className="text-foreground text-xs font-medium">user</span>
<span className="text-foreground-light text-xs">text</span>
</div>
</th>
<th className="border-b border-default px-3 py-1.5 text-left font-normal">
<div className="flex items-center gap-1.5 overflow-hidden">
<span className="text-foreground text-xs font-medium">text</span>
<span className="text-foreground-light text-xs">text</span>
</div>
</th>
</tr>
</thead>
</table>
<div
ref={tableScrollRef}
className="max-h-[58px] overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
<table className="w-full border-collapse text-sm !mt-0 table-fixed">
<colgroup>
<col style={{ width: 72 }} />
<col style={{ width: 100 }} />
<col />
</colgroup>
<tbody className="[&>tr:last-child>td]:border-b-0">
{messages.map((msg) => (
<tr
key={msg.id}
className={cn(
'transition-colors duration-500',
tableFlashId === msg.id ? 'bg-brand/15 dark:bg-brand/5' : 'bg-surface-75'
)}
>
<td className="border-b border-r border-default px-3 py-1.5 text-foreground-muted text-xs truncate max-w-0">
{msg.id}
</td>
<td className="border-b border-r border-default px-3 py-1.5 text-foreground text-xs truncate max-w-0">
{msg.user}
</td>
<td className="border-b border-default px-3 py-1.5 text-foreground text-xs truncate max-w-0">
{msg.text}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Connecting dashed lines */}
<div className="absolute left-1/2 -translate-x-1/2 top-[140px] flex gap-2 z-[1] text-background-surface-300">
<svg width="2" height="52" fill="none" className="overflow-visible">
<line
x1="1"
y1="0"
x2="1"
y2="52"
stroke="currentColor"
strokeWidth="1.5"
strokeDasharray="4 4"
style={{ animation: 'rlDashFlow 1s linear infinite' }}
/>
</svg>
<svg width="2" height="52" fill="none" className="overflow-visible">
<line
x1="1"
y1="0"
x2="1"
y2="52"
stroke="currentColor"
strokeWidth="1.5"
strokeDasharray="4 4"
style={{ animation: 'rlDashFlow 1s linear infinite 0.5s' }}
/>
</svg>
<style>{`@keyframes rlDashFlow { to { stroke-dashoffset: -8; } }`}</style>
</div>
{/* Chat UI */}
<div className="px-4 pb-4 relative z-[2]">
<div className="border border-border rounded-lg overflow-hidden bg-surface-200 max-w-[90%]">
<div
ref={chatScrollRef}
className="h-[120px] overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden flex flex-col gap-2 px-3 py-3"
style={{
maskImage: 'linear-gradient(to bottom, transparent 0%, black 20%)',
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 20%)',
}}
>
{messages.map((msg) => {
const isAlice = msg.user === 'Alice'
return (
<div key={msg.id} className={cn('flex', isAlice ? 'justify-end' : 'justify-start')}>
<div
className={cn(
'max-w-[75%] px-3 py-1.5 rounded-xl text-xs leading-snug transition-colors duration-500',
isAlice
? 'bg-brand/15 text-foreground rounded-br-sm'
: 'bg-surface-300 text-foreground rounded-bl-sm',
chatFlashId === msg.id && 'ring-1 ring-brand/30'
)}
>
{msg.text}
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
)
}
export function FeaturesSection() {
return (
<div className="py-24 flex flex-col gap-24">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Everything you need
<br />
<span className="text-foreground">from your database</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Every Supabase project is a full Postgres database with realtime functionality,
fine-grained access controls, and instant APIs — no extra configuration required.
</p>
</div>
</div>
{/* 3-col grid */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{FEATURES.map((feature) => {
const Visual = feature.visual
return (
<div
key={feature.title}
className="flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden"
>
{/* Visual with overlaid title */}
<div className="relative flex items-center justify-center h-[320px]">
<Visual />
</div>
{/* Content */}
<div className="px-6 py-5 flex flex-col gap-1">
<h4 className="text-foreground text-sm font-medium">{feature.description}</h4>
<p className="text-foreground-lighter text-sm">{feature.detail}</p>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
@@ -0,0 +1,29 @@
import Link from 'next/link'
import { Button } from 'ui'
export function Hero() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-32 pb-16 md:pt-40 md:pb-32">
<div className="flex flex-col gap-6 lg:gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h1 className="text-foreground text-3xl sm:text-5xl sm:leading-none">
<span className="block">Postgres</span>
<span className="text-foreground-lighter block">without the hassle</span>
</h1>
<p className="text-foreground-lighter text-sm lg:text-base">
Every Supabase project is a dedicated Postgres database. 100% portable with no vendor
lock-in.
</p>
</div>
<div className="flex items-center gap-2">
<Button asChild size="medium">
<Link href="https://supabase.com/dashboard">Start your project</Link>
</Button>
<Button asChild size="medium" type="default">
<Link href="/docs/guides/database/overview">Documentation</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,108 @@
import { Check } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { Badge, Button } from 'ui'
type Highlight = {
title: string
badge?: string
features: string[]
cta: { label: string; href: string }
image: { dark: string; light: string }
}
const HIGHLIGHTS: Highlight[] = [
{
title: 'Branching',
features: [
'Branch your Supabase project',
'Sync with your git branches',
'Manage every Preview from the Dashboard',
'Support for Vercel Previews',
],
cta: { label: 'Learn more', href: '/docs/guides/platform/branching' },
image: {
dark: '/images/product/database/branching.svg',
light: '/images/product/database/branching-light.svg',
},
},
{
title: 'Read Replicas',
features: [
'Serve data closer to your users',
'Provide data redundancy',
'Run complex queries without affecting your primary database',
'Distribute load across various databases',
],
cta: { label: 'Learn more', href: '/docs/guides/platform/read-replicas' },
image: {
dark: '/images/product/database/read-replicas.svg',
light: '/images/product/database/read-replicas-light.svg',
},
},
]
export function HighlightsSection() {
return (
<div className="py-24 flex flex-col gap-24">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Built for scale
<br />
<span className="text-foreground">with powerful new features</span>
</h3>
</div>
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{HIGHLIGHTS.map((highlight) => (
<div
key={highlight.title}
className="relative flex flex-col overflow-hidden min-h-[400px] bg-surface-75 border border-border rounded-lg"
>
<div className="relative z-10 flex flex-col justify-between h-full p-6 md:p-8 pr-0 md:pr-0">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<h4 className="text-foreground text-lg font-medium">{highlight.title}</h4>
{highlight.badge && <Badge variant="success">{highlight.badge}</Badge>}
</div>
<ul className="flex flex-col text-foreground-lighter text-sm gap-1.5">
{highlight.features.map((feature) => (
<li key={feature} className="flex items-start gap-2">
<Check className="w-3.5 h-3.5 mt-0.5 shrink-0 stroke-2 text-brand" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
<div className="mt-6">
<Button type="default" size="small" asChild>
<Link href={highlight.cta.href}>{highlight.cta.label}</Link>
</Button>
</div>
</div>
{/* Decorative image */}
<div className="hidden sm:flex lg:hidden xl:flex absolute top-0 right-0 bottom-0 items-end h-full opacity-50 dark:opacity-20 select-none">
<Image
src={highlight.image.dark}
alt={highlight.title}
width={296}
height={275}
className="hidden dark:block"
/>
<Image
src={highlight.image.light}
alt={highlight.title}
width={296}
height={275}
className="dark:hidden"
/>
</div>
</div>
))}
</div>
</div>
</div>
)
}
@@ -0,0 +1,196 @@
import { Key } from 'lucide-react'
const columns = [
{ name: 'id', format: 'int8', isPrimaryKey: true },
{ name: 'created_at', format: 'timestamptz' },
{ name: 'name', format: 'text' },
{ name: 'email', format: 'text' },
{ name: 'role', format: 'text' },
{ name: 'is_active', format: 'bool' },
{ name: 'avatar_url', format: 'text' },
]
const rows = [
{
id: 1,
created_at: '2024-01-15 09:23:41+00',
name: 'Alice Johnson',
email: 'alice@example.com',
role: 'admin',
is_active: 'true',
avatar_url: 'https://i.pravatar.cc/150?u=alice',
},
{
id: 2,
created_at: '2024-01-15 10:45:12+00',
name: 'Bob Smith',
email: 'bob@example.com',
role: 'editor',
is_active: 'true',
avatar_url: 'https://i.pravatar.cc/150?u=bob',
},
{
id: 3,
created_at: '2024-02-03 14:12:08+00',
name: 'Carol Williams',
email: 'carol@example.com',
role: 'viewer',
is_active: 'false',
avatar_url: 'https://i.pravatar.cc/150?u=carol',
},
{
id: 4,
created_at: '2024-02-14 08:33:55+00',
name: 'David Brown',
email: 'david@example.com',
role: 'editor',
is_active: 'true',
avatar_url: 'NULL',
},
{
id: 5,
created_at: '2024-03-01 16:07:29+00',
name: 'Eve Martinez',
email: 'eve@example.com',
role: 'admin',
is_active: 'true',
avatar_url: 'https://i.pravatar.cc/150?u=eve',
},
{
id: 6,
created_at: '2024-03-10 11:52:44+00',
name: 'Frank Lee',
email: 'frank@example.com',
role: 'viewer',
is_active: 'true',
avatar_url: 'https://i.pravatar.cc/150?u=frank',
},
{
id: 7,
created_at: '2024-03-22 07:18:03+00',
name: 'Grace Kim',
email: 'grace@example.com',
role: 'editor',
is_active: 'false',
avatar_url: 'https://i.pravatar.cc/150?u=grace',
},
{
id: 8,
created_at: '2024-04-05 13:41:17+00',
name: 'Henry Chen',
email: 'henry@example.com',
role: 'viewer',
is_active: 'true',
avatar_url: 'https://i.pravatar.cc/150?u=henry',
},
{
id: 9,
created_at: '2024-04-12 09:05:33+00',
name: 'Isla Patel',
email: 'isla@example.com',
role: 'admin',
is_active: 'true',
avatar_url: 'https://i.pravatar.cc/150?u=isla',
},
{
id: 10,
created_at: '2024-04-18 15:22:07+00',
name: 'Jack Rivera',
email: 'jack@example.com',
role: 'editor',
is_active: 'false',
avatar_url: 'NULL',
},
]
const EDITED_ROW = 1
const EDITED_COL = 'created_at'
export function SpreadsheetIllustration() {
return (
<div className="relative w-full">
<table className="w-full border-collapse text-[13px]" style={{ minWidth: 900 }}>
<thead>
<tr className="bg-surface-200">
{columns.map((col) => (
<th
key={col.name}
className="border-b border-r last:border-r-0 border-default px-3 py-1.5 text-left font-normal"
>
<div className="flex items-center gap-1.5 overflow-hidden">
{col.isPrimaryKey && (
<Key size={12} strokeWidth={2} className="text-brand rotate-45 shrink-0" />
)}
<span className="text-foreground text-xs truncate font-medium">{col.name}</span>
<span className="text-foreground-light text-xs truncate">{col.format}</span>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id} className="bg-surface-75">
{columns.map((col) => {
const value = String(row[col.name as keyof typeof row])
const isNull = value === 'NULL'
const isEdited = row.id === EDITED_ROW && col.name === EDITED_COL
return (
<td
key={col.name}
className={`border-b border-r last:border-r-0 border-secondary px-3 py-1.5 max-w-[200px] ${
isEdited ? 'bg-brand/10' : ''
}`}
>
<span
className={`block truncate ${isNull ? 'text-foreground-muted italic' : 'text-foreground'}`}
>
{value}
</span>
</td>
)
})}
</tr>
))}
</tbody>
</table>
{/* Floating cell editor */}
<div className="absolute top-[42px] left-[105px] w-[280px] rounded-lg border border-strong bg-surface-200 shadow-2xl overflow-hidden z-10">
<div className="px-3 py-2.5 border-b border-default">
<div className="rounded bg-brand-400/20 px-2 py-1 text-[11px] text-foreground font-mono">
2024-01-15 09:23:41.206064+00
</div>
</div>
<div className="px-3 py-2.5 flex flex-col gap-1">
<span className="text-[10px] text-foreground-muted">Formatted value:</span>
<span className="text-[12px] text-foreground font-mono">
15 Jan 2024 09:23:41 (+0000)
</span>
</div>
<div className="px-3 pb-3 flex flex-col gap-1.5">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<kbd className="flex items-center justify-center w-6 h-6 rounded border border-default bg-surface-300 text-foreground-light text-[10px]">
↵
</kbd>
<span className="text-[11px] text-foreground-muted">Save changes</span>
</div>
<div className="ml-auto">
<div className="rounded border border-default bg-surface-100 px-2.5 py-1 text-[10px] text-foreground-light">
Set to NOW
</div>
</div>
</div>
<div className="flex items-center gap-2">
<kbd className="flex items-center justify-center px-1.5 h-6 rounded border border-default bg-surface-300 text-foreground-light text-[10px]">
Esc
</kbd>
<span className="text-[11px] text-foreground-muted">Cancel changes</span>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,16 @@
import { QuoteSection as SharedQuoteSection } from '../../_shared/QuoteSection'
export function QuoteSection() {
return (
<SharedQuoteSection
quote="You can have a really great product, but you need to want to work with the people behind it."
highlight="With Supabase, we always felt very aligned."
author={{
name: 'Howard Haynes',
role: 'CPO at Next Door Lending',
image: '/images/blog/avatars/howard-haynes.webp',
link: '/customers/next-door-lending',
}}
/>
)
}
@@ -0,0 +1,229 @@
'use client'
import { useEffect, useRef } from 'react'
// ─── SVG paths (elephant) ────────────────────────────────────────────────────
const PATHS = [
'M192.144 125.816h-53.465c-8.506 0-16.159 5.17-19.334 13.061L99.0045 189.43c-3.0613 7.608-1.3448 16.306 4.3775 22.181l10.232 10.506c4.792 4.919 7.474 11.516 7.474 18.384l-.001 14.473c0 20.197 16.373 36.569 36.569 36.569 6.16 0 11.154-4.993 11.154-11.153l.001-86.241c0-18.629 7.441-36.486 20.668-49.602 2.746-2.723 7.178-2.704 9.9.041 2.722 2.745 2.703 7.178-.042 9.9-10.577 10.488-16.526 24.766-16.526 39.661l-.001 86.241c0 13.892-11.262 25.153-25.154 25.153-27.928 0-50.569-22.64-50.569-50.569l.001-14.474c0-3.218-1.257-6.309-3.503-8.615L93.353 221.38c-9.5904-9.847-12.4673-24.424-7.3366-37.176l20.3406-50.553c5.308-13.192 18.101-21.835 32.322-21.835h55.729v.084h10.339c49.104 0 88.91 39.806 88.91 88.91v50.842c0 3.866-3.134 7-7 7s-7-3.134-7-7V200.81c0-41.372-33.538-74.91-74.91-74.91H193.23c-.37 0-.732-.029-1.086-.084Z',
'M210.03 283.94c0-3.866-3.134-7-7-7s-7 3.134-7 7v3.113c0 26.959 21.854 48.814 48.813 48.814 26.351 0 47.825-20.879 48.781-46.996h24.614c3.866 0 7-3.134 7-7s-3.134-7-7-7h-26.841c-30.744 0-60.256-12.083-82.173-33.643-2.756-2.711-7.188-2.675-9.899.081-2.711 2.756-2.675 7.188.081 9.9 21.725 21.371 50.116 34.423 80.228 37.134-.679 18.629-15.995 33.524-34.791 33.524-19.227 0-34.813-15.587-34.813-34.814v-3.113Z',
'M238.03 202.145c0 4.792 3.885 8.677 8.677 8.677s8.676-3.885 8.676-8.677-3.884-8.676-8.676-8.676-8.677 3.884-8.677 8.676Z',
]
// ─── Shaders ─────────────────────────────────────────────────────────────────
const vert = /* glsl */ `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`
// Animated dithered noise over the filled silhouette.
const frag = /* glsl */ `
precision highp float;
uniform sampler2D tMap;
uniform float uHover;
uniform float uTime;
uniform vec2 uResolution;
varying vec2 vUv;
#define GREEN vec3(0.243, 0.812, 0.557)
// ── Noise ──
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),
f.y
);
}
float fbm(vec2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * noise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
// ── Bayer dithering (recursive 8×8) ──
float bayer2(vec2 a) {
a = floor(a);
return fract(a.x / 2.0 + a.y * a.y * 0.75);
}
float bayer4(vec2 a) { return bayer2(0.5 * a) * 0.25 + bayer2(a); }
float bayer8(vec2 a) { return bayer4(0.5 * a) * 0.25 + bayer2(a); }
void main() {
float shape = texture2D(tMap, vUv).r;
if (shape < 0.5) { gl_FragColor = vec4(0.0); return; }
// Animated FBM noise — continuous drift
vec2 noiseUv = vUv * 3.0 + vec2(uTime * 0.8, uTime * 0.5);
float n = fbm(noiseUv);
float value = n * 0.7 + 0.3;
// Bayer dithering — divide resolution for bigger dots
vec2 screenPos = vUv * uResolution / 3.0;
float dither = bayer8(screenPos);
float dithered = smoothstep(dither - 0.04, dither + 0.04, value);
vec3 col = GREEN * dithered;
float alpha = dithered * uHover;
gl_FragColor = vec4(col * alpha, alpha);
}
`
// ─── Helpers ─────────────────────────────────────────────────────────────────
function buildSVGCanvas(size: number): Promise<HTMLCanvasElement> {
return new Promise((resolve) => {
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const ctx = canvas.getContext('2d')!
const svgSrc = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 390 430" width="${size}" height="${size}">
${PATHS.map((d) => `<path d="${d}" fill="white"/>`).join('')}
</svg>`
const blob = new Blob([svgSrc], { type: 'image/svg+xml;charset=utf-8' })
const url = URL.createObjectURL(blob)
const img = new Image()
img.onload = () => {
ctx.drawImage(img, 0, 0)
URL.revokeObjectURL(url)
resolve(canvas)
}
img.src = url
})
}
// ─── Component ───────────────────────────────────────────────────────────────
interface Props {
hovered: boolean
}
export function PostgresGlow({ hovered }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const liveRef = useRef({ hovered: false, hover: 0 })
useEffect(() => {
liveRef.current.hovered = hovered
}, [hovered])
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
let running = true
let raf: number
async function init(canvas: HTMLCanvasElement) {
const { Renderer, Program, Mesh, Triangle, Texture } = await import('ogl')
if (!running) return
const svgCanvas = await buildSVGCanvas(512)
if (!running) return
const renderer = new Renderer({
canvas,
alpha: true,
premultipliedAlpha: true,
dpr: window.devicePixelRatio,
})
const gl = renderer.gl
gl.clearColor(0, 0, 0, 0)
const texture = new Texture(gl, {
image: svgCanvas,
generateMipmaps: false,
minFilter: gl.LINEAR,
magFilter: gl.LINEAR,
})
const geometry = new Triangle(gl)
const program = new Program(gl, {
vertex: vert,
fragment: frag,
uniforms: {
tMap: { value: texture },
uHover: { value: 0 },
uTime: { value: 0 },
uResolution: { value: [canvas.width, canvas.height] },
},
transparent: true,
depthTest: false,
depthWrite: false,
})
const mesh = new Mesh(gl, { geometry, program })
const container = canvas.parentElement!
function resize(entries?: ResizeObserverEntry[]) {
const w = entries?.[0]?.contentRect.width ?? container.offsetWidth
const h = entries?.[0]?.contentRect.height ?? container.offsetHeight
renderer.setSize(w, h)
program.uniforms.uResolution.value = [canvas.width, canvas.height]
}
resize()
const ro = new ResizeObserver(resize)
ro.observe(container)
function loop() {
if (!running) return
raf = requestAnimationFrame(loop)
const live = liveRef.current
const target = live.hovered ? 1 : 0
live.hover += (target - live.hover) * 0.06
program.uniforms.uHover.value = live.hover
program.uniforms.uTime.value = performance.now() / 1000
renderer.render({ scene: mesh })
}
loop()
return () => ro.disconnect()
}
let cleanupResize: (() => void) | undefined
init(canvas).then((c) => {
cleanupResize = c
})
return () => {
running = false
cancelAnimationFrame(raf)
cleanupResize?.()
}
}, [])
return (
<canvas
ref={canvasRef}
width={320}
height={320}
className="absolute inset-0 w-full h-full pointer-events-none"
style={{ mixBlendMode: 'screen' }}
/>
)
}
@@ -0,0 +1,190 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { Code, FileDown, Heart, MousePointerClick } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { Code, MousePointerClick, Heart, FileDown } as const
type IconName = keyof typeof ICONS
const TABS = [
{
icon: 'Code' as IconName,
label: 'Full SQL',
description: 'A full SQL editor built right into the dashboard.',
image: {
dark: '/images/product/database/sql-view/sql-editor.png',
light: '/images/product/database/sql-view/sql-editor-light.png',
},
},
{
icon: 'MousePointerClick' as IconName,
label: 'Monaco editor',
description: 'Built-in Monaco editor, with rich validation and autocomplete.',
image: {
dark: '/images/product/database/sql-view/monaco-editor.png',
light: '/images/product/database/sql-view/monaco-editor-light.png',
},
},
{
icon: 'Heart' as IconName,
label: 'Favorite queries',
description: 'Save your favorite queries to use again and again.',
image: {
dark: '/images/product/database/sql-view/favorites.png',
light: '/images/product/database/sql-view/favorites-light.png',
},
},
{
icon: 'FileDown' as IconName,
label: 'Export to CSV',
description: 'Any output can be exported into a CSV.',
image: {
dark: '/images/product/database/sql-view/export.png',
light: '/images/product/database/sql-view/export-csv-light.png',
},
},
]
const INTERVAL_DURATION = 8000
export function SqlEditorSection() {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = TABS[activeIdx]
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % TABS.length)
}
}, [progress])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-24">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Write and run queries with a
<br />
<span className="text-foreground">full SQL Editor</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Write, run, and save SQL queries directly from the dashboard with full autocomplete and
syntax highlighting.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tabs */}
<div className="flex flex-col gap-1 items-start justify-end">
{TABS.map((tab, index) => {
const isActive = index === activeIdx
const Icon = ICONS[tab.icon]
return (
<button
key={tab.label}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{tab.label}
</button>
)
})}
</div>
{/* Right: image + footer */}
<div className="flex flex-col border border-border rounded-md overflow-clip bg-surface-75">
<div className="relative aspect-[16/7.5] shrink-0 overflow-hidden rounded-t-md flex items-end justify-center">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.2, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
className="absolute inset-0 flex items-end justify-center px-6"
>
<img
src={active.image.dark}
alt={active.label}
className="absolute inset-0 object-cover pointer-events-none hidden dark:block"
/>
<img
src={active.image.light}
alt={active.label}
className="absolute inset-0 object-cover pointer-events-none dark:hidden"
/>
</motion.div>
</AnimatePresence>
</div>
{/* Footer: active tab info */}
<div className="border-t border-border px-6 py-4 flex items-center gap-8 justify-between bg-surface-100">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="flex flex-col gap-1"
>
<h4 className="text-sm font-medium text-foreground">{active.label}</h4>
<p className="text-sm text-foreground-lighter">{active.description}</p>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,197 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { FileDown, Link2, Table, TableProperties } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { TableProperties, Table, Link2, FileDown } as const
type IconName = keyof typeof ICONS
type Tab = {
icon: IconName
label: string
description: string
image: { dark: string; light: string }
}
const TABS: Tab[] = [
{
icon: 'TableProperties',
label: 'Spreadsheet editing',
description: 'Add, edit, and update your data with the simplicity of a no-code tool.',
image: {
dark: '/images/product/database/table-view/spreadsheet.png',
light: '/images/product/database/table-view/table-editor-light.png',
},
},
{
icon: 'Table',
label: 'Create tables',
description: 'Add tables, columns and rows without writing SQL.',
image: {
dark: '/images/product/database/table-view/create-tables.png',
light: '/images/product/database/table-view/create-table-light.png',
},
},
{
icon: 'Link2',
label: 'Foreign keys',
description: 'Build connections across tables with the full power of relational data.',
image: {
dark: '/images/product/database/table-view/foreign-keys.png',
light: '/images/product/database/table-view/foreign-keys-light.png',
},
},
{
icon: 'FileDown',
label: 'Export to CSV',
description: 'Pick the rows you want and export them into a CSV.',
image: {
dark: '/images/product/database/table-view/export-csv.png',
light: '/images/product/database/table-view/export-csv-light.png',
},
},
]
const INTERVAL_DURATION = 8000
export function TableEditorSection() {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = TABS[activeIdx]
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % TABS.length)
}
}, [progress])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-24">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Manage your data with a
<br />
<span className="text-foreground">built-in Table Editor</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Manage your data with a spreadsheet-like interface. Create tables, set up relationships,
and export — no SQL required.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tabs */}
<div className="flex flex-col gap-1 items-start justify-end">
{TABS.map((tab, index) => {
const isActive = index === activeIdx
const Icon = ICONS[tab.icon]
return (
<button
key={tab.label}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{tab.label}
</button>
)
})}
</div>
{/* Right: image + footer */}
<div className="flex flex-col border border-border rounded-md overflow-clip bg-surface-75">
<div className="relative aspect-[16/7.5] shrink-0 overflow-hidden rounded-t-md flex items-end justify-center">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.2, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
className="absolute inset-0 flex items-end justify-center px-6"
>
<img
src={active.image.dark}
alt={active.label}
className="absolute inset-0 object-cover pointer-events-none hidden dark:block"
/>
<img
src={active.image.light}
alt={active.label}
className="absolute inset-0 object-cover pointer-events-none dark:hidden"
/>
</motion.div>
</AnimatePresence>
</div>
{/* Footer: active tab info */}
<div className="border-t border-border px-6 py-4 flex items-center gap-8 justify-between bg-surface-100">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="flex flex-col gap-1"
>
<h4 className="text-sm font-medium text-foreground">{active.label}</h4>
<p className="text-sm text-foreground-lighter">{active.description}</p>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,598 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronDown, Filter, Key, List } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { Button } from 'ui'
const columns = [
{ name: 'id', format: 'int8', isPrimaryKey: true },
{ name: 'created_at', format: 'timestamptz' },
{ name: 'name', format: 'text' },
{ name: 'email', format: 'text' },
{ name: 'role', format: 'text' },
{ name: 'is_active', format: 'bool' },
{ name: 'avatar_url', format: 'text' },
]
type Row = {
id: number
created_at: string
name: string
email: string
role: string
is_active: boolean
avatar_url: string
}
const initialRows: Row[] = [
{
id: 1,
created_at: '2024-01-15 09:23:41+00',
name: 'Alice Johnson',
email: 'alice@example.com',
role: 'admin',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=alice',
},
{
id: 2,
created_at: '2024-01-15 10:45:12+00',
name: 'Bob Smith',
email: 'bob@example.com',
role: 'editor',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=bob',
},
{
id: 3,
created_at: '2024-02-03 14:12:08+00',
name: 'Carol Williams',
email: 'carol@example.com',
role: 'viewer',
is_active: false,
avatar_url: 'https://i.pravatar.cc/150?u=carol',
},
{
id: 4,
created_at: '2024-02-14 08:33:55+00',
name: 'David Brown',
email: 'david@example.com',
role: 'editor',
is_active: true,
avatar_url: 'NULL',
},
{
id: 5,
created_at: '2024-03-01 16:07:29+00',
name: 'Eve Martinez',
email: 'eve@example.com',
role: 'admin',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=eve',
},
{
id: 6,
created_at: '2024-03-10 11:52:44+00',
name: 'Frank Lee',
email: 'frank@example.com',
role: 'viewer',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=frank',
},
{
id: 7,
created_at: '2024-03-22 07:18:03+00',
name: 'Grace Kim',
email: 'grace@example.com',
role: 'editor',
is_active: false,
avatar_url: 'https://i.pravatar.cc/150?u=grace',
},
{
id: 8,
created_at: '2024-04-05 13:41:17+00',
name: 'Henry Chen',
email: 'henry@example.com',
role: 'viewer',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=henry',
},
{
id: 9,
created_at: '2024-04-12 09:05:33+00',
name: 'Isla Patel',
email: 'isla@example.com',
role: 'admin',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=isla',
},
{
id: 10,
created_at: '2024-04-18 15:22:07+00',
name: 'Jack Rivera',
email: 'jack@example.com',
role: 'editor',
is_active: false,
avatar_url: 'NULL',
},
{
id: 11,
created_at: '2024-05-02 11:48:19+00',
name: 'Karen Nguyen',
email: 'karen@example.com',
role: 'viewer',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=karen',
},
{
id: 12,
created_at: '2024-05-09 08:14:52+00',
name: "Liam O'Brien",
email: 'liam@example.com',
role: 'editor',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=liam',
},
{
id: 13,
created_at: '2024-05-21 17:33:28+00',
name: 'Mia Tanaka',
email: 'mia@example.com',
role: 'admin',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=mia',
},
{
id: 14,
created_at: '2024-06-01 10:07:41+00',
name: 'Noah Garcia',
email: 'noah@example.com',
role: 'viewer',
is_active: false,
avatar_url: 'https://i.pravatar.cc/150?u=noah',
},
{
id: 15,
created_at: '2024-06-14 14:55:03+00',
name: 'Olivia Schmidt',
email: 'olivia@example.com',
role: 'editor',
is_active: true,
avatar_url: 'NULL',
},
{
id: 16,
created_at: '2024-06-23 06:29:17+00',
name: 'Pablo Morales',
email: 'pablo@example.com',
role: 'viewer',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=pablo',
},
{
id: 17,
created_at: '2024-07-04 12:11:44+00',
name: 'Quinn Foster',
email: 'quinn@example.com',
role: 'admin',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=quinn',
},
{
id: 18,
created_at: '2024-07-15 19:38:56+00',
name: 'Ruby Aoki',
email: 'ruby@example.com',
role: 'editor',
is_active: false,
avatar_url: 'https://i.pravatar.cc/150?u=ruby',
},
{
id: 19,
created_at: '2024-07-28 07:52:10+00',
name: 'Sam Walker',
email: 'sam@example.com',
role: 'viewer',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=sam',
},
{
id: 20,
created_at: '2024-08-03 16:04:33+00',
name: 'Tara Singh',
email: 'tara@example.com',
role: 'admin',
is_active: true,
avatar_url: 'https://i.pravatar.cc/150?u=tara',
},
]
const namePool = [
'Uri Petrov',
'Vera Costa',
'Will Chang',
'Xena Müller',
'Yuki Sato',
'Zara Ahmed',
'Felix Braun',
'Luna Park',
'Oscar Diaz',
'Nina Rossi',
]
const rolePool = ['admin', 'editor', 'viewer']
const editableFields = ['name', 'email', 'role', 'is_active'] as const
const nameEdits = [
'Alex Turner',
'Sofia Reyes',
'Marcus Cole',
'Priya Sharma',
'Leo Zhang',
'Dana Okafor',
]
const emailEdits = [
'updated@example.com',
'new.email@example.com',
'changed@example.com',
'modified@example.com',
]
function randomTimestamp(): string {
const m = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')
const d = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')
const h = String(Math.floor(Math.random() * 24)).padStart(2, '0')
const min = String(Math.floor(Math.random() * 60)).padStart(2, '0')
const sec = String(Math.floor(Math.random() * 60)).padStart(2, '0')
return `2025-${m}-${d} ${h}:${min}:${sec}+00`
}
function pick<T>(arr: readonly T[]): T {
return arr[Math.floor(Math.random() * arr.length)]
}
function formatCellValue(value: unknown): string {
if (value === null || value === undefined) return 'NULL'
if (typeof value === 'boolean') return value.toString()
return String(value)
}
// ── Stable identity for each row instance ──
// Every row gets a unique _uid that never changes and is never reused.
let uidCounter = 0
type URow = Row & { _uid: string }
function stampRow(row: Row): URow {
return { ...row, _uid: `u${++uidCounter}` }
}
// ── Flash state: at most one thing highlighted at a time ──
type Flash = {
type: 'modify' | 'add' | 'delete'
uid: string // row _uid
col?: string // column name, only for 'modify'
} | null
export function VisualDemo() {
// All rows carry a stable _uid used as the sole React key.
const [initialStamped] = useState<URow[]>(() => initialRows.map(stampRow))
const [rows, setRows] = useState<URow[]>(initialStamped)
const [introComplete, setIntroComplete] = useState(false)
const [flash, setFlash] = useState<Flash>(null)
const [isInView, setIsInView] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const el = containerRef.current
if (!el) return
const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), {
threshold: 0.1,
})
observer.observe(el)
return () => observer.disconnect()
}, [])
// Map from _uid → stagger order for intro animation
const introOrder = useRef<Record<string, number>>({})
if (Object.keys(introOrder.current).length === 0) {
initialStamped.forEach((r, i) => {
introOrder.current[r._uid] = i
})
}
// Mutable refs so the action loop never needs to re-create.
const rowsRef = useRef(rows)
rowsRef.current = rows
const nextId = useRef(initialRows.length + 1)
const deleted = useRef<URow[]>([])
// Per-cell version counter: bumped on every modify so the AnimatePresence
// key always changes, even if the display value happens to stay the same.
const cellVer = useRef<Record<string, number>>({})
function bumpCellVer(uid: string, col: string) {
const k = `${uid}.${col}`
cellVer.current[k] = (cellVer.current[k] || 0) + 1
}
function getCellVer(uid: string, col: string) {
return cellVer.current[`${uid}.${col}`] || 0
}
// ── Intro stagger ──
const introMs = (initialRows.length * 0.025 + 0.36) * 1000 + 200
useEffect(() => {
const t = setTimeout(() => setIntroComplete(true), introMs)
return () => clearTimeout(t)
}, [introMs])
// ── Action loop — one action at a time, only while in view ──
useEffect(() => {
if (!introComplete || !isInView) return
let cancelled = false
let timer: ReturnType<typeof setTimeout>
function schedule() {
const delay = 1500 + Math.random() * 2500
timer = setTimeout(() => {
if (cancelled) return
runAction(() => {
if (!cancelled) schedule()
})
}, delay)
}
function runAction(onDone: () => void) {
const cur = rowsRef.current
const actions = ['modify', 'modify', 'delete', 'add'] as const
let action = pick(actions)
if (action === 'delete' && cur.length <= 20) action = 'add'
if (action === 'modify') {
const row = pick(cur)
const field = pick(editableFields)
const updated = { ...row }
if (field === 'name') updated.name = pick(nameEdits)
else if (field === 'email') {
const u = updated.name.toLowerCase().split(' ')[0]
updated.email = Math.random() > 0.5 ? `${u}@example.com` : pick(emailEdits)
} else if (field === 'role') {
updated.role = pick(rolePool.filter((r) => r !== updated.role))
} else {
updated.is_active = !updated.is_active
}
bumpCellVer(row._uid, field)
setFlash({ type: 'modify', uid: row._uid, col: field })
setRows((prev) => prev.map((r) => (r._uid === row._uid ? updated : r)))
setTimeout(() => {
setFlash(null)
onDone()
}, 800)
return
}
if (action === 'delete') {
const row = pick(cur)
// Flash red and remove simultaneously — flash stays visible during exit animation
setFlash({ type: 'delete', uid: row._uid })
deleted.current.push(row)
setRows((prev) => prev.filter((r) => r._uid !== row._uid))
setTimeout(() => {
setFlash(null)
onDone()
}, 600)
return
}
// ── add ──
let base: Row
if (deleted.current.length > 0 && Math.random() > 0.3) {
const i = Math.floor(Math.random() * deleted.current.length)
base = { ...deleted.current[i], id: nextId.current++ }
deleted.current.splice(i, 1)
} else {
const name = pick(namePool)
const u = name.toLowerCase().split(' ')[0]
base = {
id: nextId.current++,
created_at: randomTimestamp(),
name,
email: `${u}@example.com`,
role: pick(rolePool),
is_active: Math.random() > 0.3,
avatar_url: Math.random() > 0.2 ? `https://i.pravatar.cc/150?u=${u}` : 'NULL',
}
}
const newRow = stampRow(base)
setFlash({ type: 'add', uid: newRow._uid })
setRows((prev) => {
const pos = Math.floor(Math.random() * (prev.length + 1))
const next = [...prev]
next.splice(pos, 0, newRow)
return next
})
setTimeout(() => {
setFlash(null)
onDone()
}, 800)
}
schedule()
return () => {
cancelled = true
clearTimeout(timer)
}
}, [introComplete, isInView])
return (
<div ref={containerRef} className="border-t border-border relative">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pb-16 md:pb-24">
<div className="pt-8 md:pt-0 md:-translate-y-16 relative w-full h-[620px]">
{/* Gradient blob behind card */}
<div
className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[105%] w-[102%] rounded-[300px] blur-[50px] dark:blur-[24px] dark:saturate-[0.4] dark:brightness-50"
style={{
backgroundImage:
'linear-gradient(90deg, rgba(62, 207, 142, 0.2) 0%, rgba(151, 62, 198, 0.2) 50%, rgba(62, 207, 142, 0.2) 100%)',
}}
/>
{/* Card */}
<div
className="relative z-10 w-full h-full rounded-xl border border-border bg-surface-75 overflow-hidden flex flex-col"
style={{
boxShadow:
'0 0 0 0.5px var(--shadow-border-color), 0 149px 199px 0 rgba(0,0,0,0.07), 0 62.249px 83.137px 0 rgba(0,0,0,0.05), 0 33.281px 44.449px 0 rgba(0,0,0,0.04), 0 18.657px 24.918px 0 rgba(0,0,0,0.04), 0 9.909px 13.234px 0 rgba(0,0,0,0.03), 0 4.123px 5.507px 0 rgba(0,0,0,0.02)',
}}
>
{/* Toolbar */}
<div className="flex items-center gap-2 px-1.5 h-10 border-b border-default bg-surface-200 shrink-0">
<button className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-foreground-light hover:text-foreground transition-colors">
<Filter size={14} strokeWidth={1.5} />
<span>Filter</span>
</button>
<button className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-foreground-light hover:text-foreground transition-colors">
<List size={14} strokeWidth={1.5} />
<span>Sort</span>
</button>
<Button
size="tiny"
className="ml-auto"
iconRight={<ChevronDown size={12} strokeWidth={2} />}
>
Insert
</Button>
</div>
{/* Grid */}
<div
className="overflow-hidden"
style={{
maskImage: 'linear-gradient(to bottom, rgba(0,0,0,0.85) 80%, transparent 100%)',
WebkitMaskImage:
'linear-gradient(to bottom, rgba(0,0,0,0.85) 80%, transparent 100%)',
}}
>
<div className="overflow-hidden">
<table
className="w-full border-collapse text-[13px] !mt-0"
style={{ minWidth: 900 }}
>
<thead>
<tr className="bg-surface-200">
{columns.map((col) => (
<th
key={col.name}
className="border-b border-r last:border-r-0 border-default px-3 py-1.5 text-left font-normal"
>
<div className="flex items-center gap-1.5 overflow-hidden">
{col.isPrimaryKey && (
<Key
size={12}
strokeWidth={2}
className="text-brand rotate-45 shrink-0"
/>
)}
<span className="text-foreground text-xs truncate font-medium">
{col.name}
</span>
<span className="text-foreground-light text-xs truncate">
{col.format}
</span>
</div>
</th>
))}
</tr>
</thead>
<tbody>
<AnimatePresence initial={true}>
{rows.map((row) => (
<motion.tr
key={row._uid}
layout
className="bg-surface-75"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={
!introComplete
? {
opacity: {
duration: 0.36,
delay: (introOrder.current[row._uid] ?? 0) * 0.025,
ease: [0.25, 0.1, 0.25, 1],
},
height: { duration: 0 },
}
: {
layout: { duration: 0.3, ease: [0.25, 0.1, 0.25, 1] },
opacity: { duration: 0.2 },
height: { duration: 0.3 },
}
}
>
{columns.map((col) => {
const value = row[col.name as keyof Row]
const displayValue = formatCellValue(value)
const isNull = displayValue === 'NULL'
const isBool = col.format === 'bool'
const isModifiedCell =
flash?.type === 'modify' &&
flash.uid === row._uid &&
flash.col === col.name
const isAddedRow = flash?.type === 'add' && flash.uid === row._uid
const isDeletedRow = flash?.type === 'delete' && flash.uid === row._uid
const cellBg = isDeletedRow
? 'hsl(var(--destructive-default) / 0.15)'
: isModifiedCell || isAddedRow
? 'hsl(var(--brand-default) / 0.15)'
: undefined
const ver = getCellVer(row._uid, col.name)
return (
<td
key={col.name}
className="border-b border-r last:border-r-0 border-secondary px-3 py-1.5 max-w-[200px] transition-colors duration-500"
style={cellBg ? { backgroundColor: cellBg } : undefined}
>
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={`${row._uid}.${col.name}.${ver}`}
className={`block truncate ${
isNull ? 'text-foreground-muted italic' : 'text-foreground'
}`}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.15 }}
>
{isNull
? 'NULL'
: isBool
? value
? 'true'
: 'false'
: displayValue}
</motion.span>
</AnimatePresence>
</td>
)
})}
</motion.tr>
))}
</AnimatePresence>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
+14
View File
@@ -0,0 +1,14 @@
import type { Metadata } from 'next'
import { ApiSection } from './_components/ApiSection'
import { DatabaseContent } from './_components/DatabaseContent'
export const metadata: Metadata = {
title: 'Database | Supabase',
description:
'Every Supabase project is a dedicated Postgres database. 100% portable with no vendor lock-in.',
}
export default function DatabasePage() {
return <DatabaseContent apiSlot={<ApiSection />} />
}
@@ -0,0 +1,92 @@
'use client'
import { AnimatePresence, motion, useInView } from 'framer-motion'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { Button } from 'ui'
const TERMINAL_LINES = [
{ text: '$ supabase functions new hello-world', delay: 0 },
{ text: 'Created function: supabase/functions/hello-world/index.ts', delay: 0.3, dim: true },
{ text: '', delay: 0.5 },
{ text: '$ supabase functions deploy hello-world', delay: 0.6 },
{ text: 'Bundling function...', delay: 0.9, dim: true },
{ text: 'Deploying to 30+ regions...', delay: 1.1, dim: true },
{ text: '', delay: 1.3 },
{ text: '✓ Function deployed:', delay: 1.4, accent: true },
{ text: ' https://your-project.supabase.co/functions/v1/hello-world', delay: 1.5, accent: true },
]
function TerminalAnimation() {
const ref = useRef<HTMLDivElement>(null)
const inView = useInView(ref, { once: true, amount: 0.5 })
const [visibleCount, setVisibleCount] = useState(0)
useEffect(() => {
if (!inView) return
const timeouts: ReturnType<typeof setTimeout>[] = []
TERMINAL_LINES.forEach((line, i) => {
timeouts.push(setTimeout(() => setVisibleCount(i + 1), line.delay * 1000))
})
return () => timeouts.forEach(clearTimeout)
}, [inView])
return (
<div
ref={ref}
className="w-full max-w-xl mx-auto rounded-lg border border-border bg-surface-75 overflow-hidden"
>
{/* Title bar */}
<div className="flex items-center gap-1.5 px-2 py-2 border-b border-border bg-surface-200">
<span className="ml-2 text-xs text-foreground-muted font-mono uppercase">Terminal</span>
</div>
{/* Lines */}
<div className="px-4 md:px-6 font-mono text-xs md:text-sm leading-relaxed h-[280px] flex flex-col justify-start items-start text-left pt-4">
<AnimatePresence>
{TERMINAL_LINES.slice(0, visibleCount).map((line, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 1 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className={
line.accent ? 'text-brand' : line.dim ? 'text-foreground-light' : 'text-foreground'
}
>
{line.text || '\u00A0'}
</motion.div>
))}
</AnimatePresence>
</div>
</div>
)
}
export function CTASection() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)]">
<div className="flex flex-col items-center text-center gap-10 py-24 md:py-32 px-6">
<div className="flex flex-col items-center gap-4">
<h2 className="text-2xl md:text-4xl text-foreground">
Deploy your first function
<span className="block text-foreground-lighter">in under a minute</span>
</h2>
<p className="text-foreground-lighter text-sm max-w-md">
Two commands. Global deployment. No infrastructure to manage.
</p>
</div>
<TerminalAnimation />
<div className="flex items-center gap-2">
<Button asChild size="medium">
<Link href="https://supabase.com/dashboard">Start your project</Link>
</Button>
<Button asChild size="medium" type="default">
<Link href="/docs/guides/functions/quickstart">Quickstart guide</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,50 @@
import { QuoteSection } from '../../_shared/QuoteSection'
import { CTASection } from './CTASection'
import { ExamplesSection } from './ExamplesSection'
import { GlobalPresenceSection } from './GlobalPresenceSection'
import { Hero } from './Hero'
import { Highlights } from './Highlights'
import { IntegratesSection } from './IntegratesSection'
import { LocalDXSection } from './LocalDXSection'
import { ObservabilitySection } from './ObservabilitySection'
export function EdgeFunctionsContent() {
return (
<div className="overflow-x-clip">
<section className="pb-16 md:pb-32">
<Hero />
<Highlights />
</section>
<section className="border-t border-border">
<QuoteSection
quote="Supabase gave us the flexibility and scalability needed at every growth stage."
highlight="It's rare to find a tool that works just as well for startups as it does for large-scale operations."
author={{
name: 'Zeno Rocha',
role: 'CEO at Resend',
image: '/images/blog/avatars/zeno-rocha.png',
link: '/customers/resend',
}}
/>
</section>
<section className="border-t border-border">
<ExamplesSection />
</section>
<section className="border-t border-border">
<LocalDXSection />
</section>
<section className="border-t border-border">
<GlobalPresenceSection />
</section>
<section className="border-t border-border">
<ObservabilitySection />
</section>
<section className="border-y border-border">
<IntegratesSection />
</section>
<section className="">
<CTASection />
</section>
</div>
)
}
@@ -0,0 +1,103 @@
import Examples from 'data/Examples'
import Link from 'next/link'
import { PRODUCT_NAMES } from 'shared-data/products'
const EXAMPLE_ICON: Record<string, { src: string; invert?: boolean; className?: string }> = {
Resend: { src: '/images/logos/resend-wordmark.svg', invert: true, className: 'h-4 w-auto' },
OpenAI: { src: '/images/logos/frameworks/openai.svg', invert: true, className: 'h-5 w-5' },
Stripe: { src: '/images/logos/frameworks/stripe.svg', className: 'h-7 w-auto' },
Postgres: { src: '/images/product/database/postgresql-icon.svg', className: 'h-5 w-5' },
Huggingface: { src: '/images/logos/hugging-face-icon.svg', className: 'h-5 w-5' },
Supabase: { src: '/images/supabase-logo-icon.svg', className: 'h-5 w-5' },
}
export function ExamplesSection() {
const examples = Examples.filter((example) => example.products.includes(PRODUCT_NAMES.FUNCTIONS))
const featuredExamples = examples.slice(0, 2)
const gridExamples = examples.slice(2, 6)
return (
<div className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<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>
</h3>
<Link
href="/docs/guides/functions#examples"
className="text-sm text-foreground-light hover:text-foreground underline"
>
View all examples
</Link>
</div>
</div>
{/* Cards */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
{/* Featured row - 2 large cells */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{featuredExamples.map((example) => {
const tag = example.tags?.[0]
const icon = tag ? EXAMPLE_ICON[tag] : undefined
return (
<Link
key={example.title}
href={example.repo_url ?? '#'}
target="_blank"
rel="noopener noreferrer"
className="group flex flex-col bg-surface-75 border border-border rounded-lg px-6 py-8 "
>
<div className="flex items-center gap-2 mb-4">
{icon && (
<img
src={icon.src}
alt=""
className={`${icon.className} object-contain${icon.invert ? ' dark:invert' : ''}`}
/>
)}
</div>
<h4 className="text-foreground text-sm font-medium">{example.title}</h4>
<p className="text-foreground-lighter text-sm mt-1 line-clamp-2">
{example.description}
</p>
</Link>
)
})}
</div>
{/* Grid row - 4 smaller cells */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 mt-3">
{gridExamples.map((example) => {
const tag = example.tags?.[0]
const icon = tag ? EXAMPLE_ICON[tag] : undefined
return (
<Link
key={example.title}
href={example.repo_url ?? '#'}
target="_blank"
rel="noopener noreferrer"
className="group flex flex-col bg-surface-75 border border-border rounded-lg px-6 py-10 "
>
<div className="flex items-center gap-2 mb-4">
{icon && (
<img
src={icon.src}
alt=""
className={`${icon.className} object-contain${icon.invert ? ' dark:invert' : ''}`}
/>
)}
</div>
<h4 className="text-foreground text-sm font-medium">{example.title}</h4>
<p className="text-foreground-lighter text-sm mt-1 line-clamp-2">
{example.description}
</p>
</Link>
)
})}
</div>
</div>
</div>
)
}
@@ -0,0 +1,51 @@
import Link from 'next/link'
const features = [
{
label: 'Global presence',
paragraph: (
<>
Edge functions run globally or can be{' '}
<Link
href="https://supabase.com/docs/guides/functions/regional-invocation"
className="underline hover:text-foreground-light transition-colors"
>
pinned to your database's proximity
</Link>
</>
),
},
{
label: 'Automatic scaling',
paragraph: 'Seamlessly scale with usage without any manual tuning',
},
{
label: 'Secure',
paragraph: 'Scale with confidence with SSL, Firewall and DDOS protection built in',
},
]
export function GlobalPresenceSection() {
return (
<div className="mx-auto max-w-[75rem] px-6 py-16 md:py-32">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 md:gap-16">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Edge Functions run{' '}
<span className="text-foreground">server-side logic geographically close to users</span>,
offering low latency and great performance.
</h3>
<div className="flex flex-col gap-6">
{features.map((feature) => (
<div
key={feature.label}
className="flex flex-col gap-1 pl-3 border-l border-border text-sm"
>
<p className="font-mono uppercase tracking-wide text-foreground">{feature.label}</p>
<p className="text-foreground-lighter">{feature.paragraph}</p>
</div>
))}
</div>
</div>
</div>
)
}
@@ -0,0 +1,29 @@
import Link from 'next/link'
import { Button } from 'ui'
export function Hero() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-32 pb-16 md:pt-40 md:pb-32">
<div className="flex flex-col gap-6 lg:gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h1 className="text-foreground text-3xl sm:text-5xl sm:leading-none">
<span className="block">Edge Functions</span>
<span className="text-foreground-lighter block">globally in seconds</span>
</h1>
<p className="text-foreground-lighter text-sm lg:text-base">
Author, deploy, and monitor serverless functions distributed globally at the edge, close
to your users.
</p>
</div>
<div className="flex items-center gap-2">
<Button asChild size="medium">
<Link href="https://supabase.com/dashboard">Start your project</Link>
</Button>
<Button asChild size="medium" type="default">
<Link href="/docs/guides/functions">Documentation</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,106 @@
import { Globe } from 'lucide-react'
const highlights = [
{
title: 'Fully managed',
paragraph: <>Edge Functions scale automatically, reducing your devops burden</>,
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.8217 2.21899C9.75065 2.21899 8.07174 3.89791 8.07174 5.96896V11.783C8.07174 11.8536 8.08148 11.9218 8.0997 11.9866C8.17354 12.3232 8.47349 12.5752 8.83229 12.5752H10.5494C10.8626 12.5752 11.1749 12.8513 11.1749 13.2656V15.0638C11.1749 15.3165 11.2998 15.54 11.4913 15.6759C11.6177 15.7729 11.7759 15.8306 11.9476 15.8306H17.9997C20.0707 15.8306 21.7496 14.1517 21.7496 12.0806V5.96896C21.7496 3.89791 20.0707 2.21899 17.9997 2.21899H11.8217ZM12.6749 14.3307H17.9997C19.2423 14.3307 20.2497 13.3233 20.2497 12.0806L20.2497 5.96896C20.2497 4.7263 19.2423 3.71893 17.9997 3.71893H11.8217C10.579 3.71893 9.57167 4.7263 9.57167 5.96896V11.0752H10.5494C10.9115 11.0752 11.2478 11.1666 11.5412 11.3263C11.5507 11.3157 11.5605 11.3052 11.5708 11.295L15.5026 7.36084L12.6816 7.36066C12.2674 7.36063 11.9316 7.02482 11.9317 6.61061C11.9317 6.1964 12.2675 5.86063 12.6817 5.86066L16.543 5.86091C17.3714 5.86096 18.0429 6.53248 18.0429 7.36085L18.0431 11.2597C18.0431 11.6739 17.7073 12.0097 17.2931 12.0097C16.8789 12.0097 16.5431 11.674 16.5431 11.2598L16.543 8.44186L12.6317 12.3553C12.597 12.3901 12.5597 12.4207 12.5205 12.4472C12.6204 12.7022 12.6749 12.9791 12.6749 13.2656V14.3307Z"
fill="currentColor"
/>
<path
d="M4.53957 11.0752H6.25666C6.67087 11.0752 7.00666 11.411 7.00666 11.8252C7.00666 12.2394 6.67087 12.5752 6.25666 12.5752H4.53957C4.22636 12.5752 3.91406 12.8513 3.91406 13.2656L3.91406 15.0638C3.91406 15.4781 3.57828 15.8138 3.16406 15.8138C2.74985 15.8138 2.41406 15.4781 2.41406 15.0638V13.2656C2.41406 12.0889 3.33344 11.0752 4.53957 11.0752Z"
fill="currentColor"
/>
<path
d="M3.16406 17.0111C3.57828 17.0111 3.91406 17.3469 3.91406 17.7611L3.91406 19.5593C3.91406 19.9737 4.22636 20.2498 4.53957 20.2498L6.25666 20.2498C6.67087 20.2498 7.00666 20.5856 7.00666 20.9998C7.00666 21.414 6.67087 21.7498 6.25666 21.7498L4.53957 21.7498C3.33344 21.7498 2.41406 20.7361 2.41406 19.5593V17.7611C2.41406 17.3469 2.74985 17.0111 3.16406 17.0111Z"
fill="currentColor"
/>
<path
d="M11.9249 17.0111C12.3391 17.0111 12.6749 17.3469 12.6749 17.7611V19.5593C12.6749 20.7361 11.7555 21.7498 10.5494 21.7498H8.83229C8.41807 21.7498 8.08229 21.414 8.08229 20.9998C8.08229 20.5856 8.41807 20.2498 8.83229 20.2498H10.5494C10.8626 20.2498 11.1749 19.9737 11.1749 19.5593V17.7611C11.1749 17.3469 11.5107 17.0111 11.9249 17.0111Z"
fill="currentColor"
/>
</svg>
),
},
{
title: 'Global deployments',
paragraph: 'Deploy worldwide for maximum resiliency and low latency',
icon: <Globe className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Secure and Scalable',
paragraph: 'Simply write your code in TypeScript and deploy',
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.7377 6.5363C14.145 6.61196 14.4138 7.00343 14.3381 7.41068L12.5869 16.8366C12.5112 17.2438 12.1197 17.5126 11.7125 17.437C11.3052 17.3613 11.0364 16.9698 11.1121 16.5626L12.8633 7.13669C12.939 6.72944 13.3305 6.46064 13.7377 6.5363Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.7279 9.10834C16.0208 8.81543 16.4957 8.81539 16.7886 9.10826L19.1499 11.4692C19.2906 11.6099 19.3696 11.8007 19.3696 11.9996C19.3696 12.1985 19.2906 12.3893 19.15 12.5299L16.7886 14.8913C16.4957 15.1842 16.0209 15.1842 15.728 14.8913C15.4351 14.5984 15.4351 14.1235 15.728 13.8306L17.5589 11.9997L15.728 10.169C15.4351 9.87613 15.4351 9.40126 15.7279 9.10834Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.72153 14.8913C9.42866 15.1842 8.95379 15.1842 8.66087 14.8913L6.29954 12.5303C6.15887 12.3897 6.07983 12.1989 6.07983 12C6.07982 11.8011 6.15884 11.6103 6.2995 11.4696L8.66083 9.10831C8.95373 8.81542 9.4286 8.81542 9.72149 9.10831C10.0144 9.4012 10.0144 9.87608 9.72149 10.169L7.89053 11.9999L9.72145 13.8306C10.0144 14.1235 10.0144 14.5983 9.72153 14.8913Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.95312 6C2.95312 3.92893 4.63206 2.25 6.70313 2.25H18.7031C20.7742 2.25 22.4531 3.92893 22.4531 6V18C22.4531 20.0711 20.7742 21.75 18.7031 21.75H6.70313C4.63206 21.75 2.95312 20.0711 2.95312 18V6ZM6.70313 3.75C5.46048 3.75 4.45313 4.75736 4.45313 6V18C4.45313 19.2426 5.46048 20.25 6.70313 20.25H18.7031C19.9458 20.25 20.9531 19.2426 20.9531 18V6C20.9531 4.75736 19.9458 3.75 18.7031 3.75H6.70313Z"
fill="currentColor"
/>
</svg>
),
},
{
title: 'Node.js Support',
paragraph: 'Migrate existing workloads with NPM and Node.js support',
icon: (
<svg className="w-5 h-5" viewBox="0 0 26 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11.9233 0.444469C12.6102 0.0512999 13.5049 0.049524 14.1912 0.444469C17.6439 2.39542 21.0976 4.3433 24.5497 6.29543C25.199 6.66117 25.6333 7.38378 25.6267 8.13193V19.8696C25.6315 20.6487 25.1543 21.3886 24.4693 21.746C21.028 23.6862 17.5884 25.6288 14.1477 27.569C13.4466 27.9698 12.5339 27.939 11.8547 27.5052C10.823 26.9072 9.78955 26.312 8.75774 25.7145C8.5469 25.5888 8.30922 25.4888 8.16034 25.2844C8.29195 25.107 8.52726 25.0849 8.71847 25.0074C9.14913 24.8705 9.54467 24.6507 9.9403 24.4373C10.0403 24.3689 10.1625 24.3951 10.2584 24.4564C11.1406 24.9622 12.0151 25.4828 12.9003 25.9838C13.0891 26.0928 13.2803 25.9481 13.4417 25.8581C16.8187 23.9495 20.1999 22.048 23.5763 20.1388C23.7014 20.0786 23.7706 19.9458 23.7604 19.8088C23.7628 15.9368 23.761 12.0641 23.7616 8.19212C23.7759 8.03662 23.6859 7.89366 23.5453 7.83003C20.1159 5.89871 16.6883 3.96444 13.2595 2.03263C13.2001 1.99179 13.1297 1.96988 13.0576 1.96977C12.9855 1.96967 12.9151 1.99138 12.8556 2.03204C9.42677 3.96444 5.99974 5.90049 2.57093 7.8317C2.43083 7.89544 2.3368 8.03603 2.35348 8.19212C2.35407 12.0641 2.35348 15.9368 2.35348 19.8094C2.34753 19.8763 2.36222 19.9433 2.39556 20.0016C2.4289 20.0599 2.47929 20.1065 2.53995 20.1353C3.45494 20.6541 4.37112 21.1694 5.2867 21.6864C5.80251 21.9641 6.43582 22.129 7.00412 21.9163C7.50562 21.7365 7.85715 21.2247 7.84758 20.6922C7.85232 16.8428 7.84521 12.9928 7.85113 9.14401C7.8386 8.97313 8.0007 8.83194 8.16685 8.84802C8.60649 8.84506 9.04672 8.8421 9.48636 8.84921C9.66987 8.84506 9.79616 9.02907 9.77347 9.20123C9.77169 13.075 9.7782 16.9489 9.77051 20.8227C9.77169 21.8551 9.34754 22.9784 8.39259 23.4836C7.21614 24.093 5.76206 23.9638 4.59982 23.3794C3.59366 22.8772 2.63348 22.2845 1.64518 21.7465C0.958393 21.391 0.483532 20.6482 0.488366 19.8697V8.13193C0.481164 7.36829 0.932741 6.63256 1.60226 6.27215C5.04301 4.33038 8.48316 2.38713 11.9233 0.444469Z"
fill="currentColor"
/>
<path
d="M14.9242 8.57537C16.4248 8.47878 18.0313 8.51814 19.3817 9.25742C20.4272 9.82394 21.0069 11.0129 21.0253 12.1745C20.9961 12.3311 20.8323 12.4176 20.6828 12.4068C20.2474 12.4062 19.8119 12.4127 19.3765 12.4039C19.1918 12.411 19.0844 12.2407 19.0613 12.0774C18.9362 11.5216 18.6331 10.9712 18.11 10.7031C17.3069 10.3011 16.3759 10.3213 15.5002 10.3297C14.861 10.3636 14.1736 10.419 13.6321 10.7949C13.2163 11.0796 13.09 11.6544 13.2384 12.1173C13.3783 12.4497 13.762 12.557 14.076 12.6558C15.8844 13.1288 17.8008 13.0818 19.5748 13.7042C20.3092 13.958 21.0277 14.4513 21.2791 15.2203C21.6079 16.2509 21.4638 17.4828 20.7305 18.3102C20.1359 18.991 19.2698 19.3616 18.406 19.5629C17.2569 19.8191 16.0644 19.8256 14.8974 19.7119C13.8001 19.5868 12.6582 19.2985 11.8111 18.5508C11.0867 17.9218 10.7329 16.9418 10.768 15.996C10.7764 15.8362 10.9354 15.7248 11.0885 15.7379C11.5269 15.7344 11.9654 15.7332 12.4038 15.7385C12.579 15.726 12.7088 15.8774 12.7178 16.0424C12.7987 16.5719 12.9977 17.1277 13.4594 17.4416C14.3505 18.0166 15.4687 17.9772 16.489 17.9933C17.3344 17.9558 18.2833 17.9444 18.9732 17.3857C19.3371 17.067 19.445 16.5339 19.3466 16.0752C19.24 15.6879 18.8349 15.5075 18.487 15.3895C16.7017 14.8247 14.7639 15.0297 12.9959 14.391C12.2781 14.1374 11.584 13.6578 11.3083 12.9204C10.9235 11.8767 11.0998 10.5858 11.9099 9.78635C12.6998 8.99113 13.8401 8.68498 14.9242 8.57537Z"
fill="currentColor"
/>
</svg>
),
},
]
export function Highlights() {
return (
<div className="border-t border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="-translate-y-16">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
{highlights.map((highlight) => (
<div
key={highlight.title}
className="bg-surface-75 border border-border rounded-lg p-6 flex flex-col gap-3"
>
<div className="text-foreground-lighter">{highlight.icon}</div>
<h3 className="text-foreground text-base font-medium">{highlight.title}</h3>
<p className="text-foreground-lighter text-sm">{highlight.paragraph}</p>
</div>
))}
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,212 @@
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { IntegratesSectionClient } from './IntegratesSectionClient'
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#bda4ff' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#3ecf8e' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#3ecf8e' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#3ecf8e' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#ffffff' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
],
}
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#6b35dc' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#15593b' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#15593b' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#15593b' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#525252' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
],
}
const USE_CASES = [
{
icon: 'Zap' as const,
label: 'Zero configuration',
paragraph: 'Pre-populated environment variables required to access your Supabase project',
lang: 'javascript' as const,
code: `import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async () => {
// These are automatically available — no setup needed
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!
)
const { data } = await supabase.from('todos').select('*')
return new Response(JSON.stringify(data))
})`,
},
{
icon: 'Database' as const,
label: 'Connect to your database',
paragraph:
'Connect to your Postgres database from an Edge Function using the supabase-js client',
lang: 'javascript' as const,
code: `const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{
global: {
headers: {
Authorization: req.headers.get('Authorization')!
}
}
}
)
const { data, error } = await supabase
.from('countries')
.select('*')`,
},
{
icon: 'Webhook' as const,
label: 'Trigger via webhook',
paragraph:
'Database Webhooks send real-time data from your database whenever a table event occurs',
lang: 'sql' as const,
code: `create trigger "my_webhook" after insert
on "public"."my_table" for each row
execute function "supabase_functions"."http_request"(
'http://localhost:3000',
'POST',
'{"Content-Type":"application/json"}',
'{}',
'1000'
);`,
},
{
icon: 'Shield' as const,
label: 'Works with Supabase Auth',
paragraph: 'Verify users and enforce permissions directly inside your Edge Functions',
lang: 'javascript' as const,
code: `import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (req: Request) => {
// Create supabase client
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
{ global: {
headers: {
Authorization: req.headers.get('Authorization')!}
}
}
)
// Get the session or user object
const { data } = await supabase.auth.getUser()
const user = data.user
})`,
},
{
icon: 'HardDrive' as const,
label: 'Works with Supabase Storage',
paragraph: 'Upload, transform, and serve files with built-in caching from your Edge Functions',
lang: 'javascript' as const,
code: `import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (req: Request) => {
// Create supabase client
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Upload image to storage
const { data, error } = await supabase.storage
.from('images')
.upload('filename.png', file, {
contentType: 'image/png',
cacheControl: '31536000',
upsert: false,
})
})`,
},
]
export async function IntegratesSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: ['javascript', 'sql'],
})
const useCases = USE_CASES.map((useCase) => ({
icon: useCase.icon,
label: useCase.label,
paragraph: useCase.paragraph,
darkHtml: hl.codeToHtml(useCase.code, { lang: useCase.lang, theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(useCase.code, { lang: useCase.lang, theme: 'supabase-light' }),
}))
hl.dispose()
return <IntegratesSectionClient useCases={useCases} />
}
@@ -0,0 +1,218 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { Database, HardDrive, Shield, Webhook, Zap } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { Zap, Database, Webhook, Shield, HardDrive } as const
type IconName = keyof typeof ICONS
type UseCase = {
icon: IconName
label: string
paragraph: React.ReactNode
darkHtml: string
lightHtml: string
}
const INTERVAL_DURATION = 6000 // ms per tab
export function IntegratesSectionClient({ useCases }: { useCases: UseCase[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = useCases[activeIdx]
// Auto-size the code area to the active snippet. We measure an always-mounted,
// invisible copy of the current code so the target height is known immediately on
// switch (and doesn't collapse during the cross-fade), then animate `height` to it.
const measureRef = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState<number>()
const hasMeasured = useRef(false)
useEffect(() => {
const el = measureRef.current
if (!el) return
const ro = new ResizeObserver(() => setHeight(el.offsetHeight))
ro.observe(el)
setHeight(el.offsetHeight)
return () => ro.disconnect()
}, [])
useEffect(() => {
if (height != null) hasMeasured.current = true
}, [height])
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
// Auto-advance when progress completes
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % useCases.length)
}
}, [progress, useCases.length])
// Start/stop timer based on visibility
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Integrates with the <span className="text-foreground">Supabase ecosystem</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Access your database, auth, storage, and webhooks directly from Edge Functions with zero
configuration.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tab cards */}
<div className="flex flex-col gap-1 items-start justify-end">
{useCases.map((useCase, index) => {
const isActive = index === activeIdx
const Icon = ICONS[useCase.icon]
return (
<button
key={useCase.label}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{useCase.label}
</button>
)
})}
</div>
{/* Right: code area */}
<div className="flex flex-col self-end border border-border rounded-md overflow-clip">
<motion.div
initial={false}
animate={height != null ? { height } : undefined}
transition={
hasMeasured.current ? { duration: 0.35, ease: [0.22, 1, 0.36, 1] } : { duration: 0 }
}
className="relative shrink-0 overflow-hidden"
>
{/* Invisible measuring copy: drives the animated code-area height */}
<div
ref={measureRef}
aria-hidden
className="pointer-events-none invisible absolute inset-x-0 top-0 overflow-x-auto [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
/>
<AnimatePresence mode="wait">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="overflow-x-auto"
>
<div
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
className="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
<div
dangerouslySetInnerHTML={{ __html: active.lightHtml }}
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
</motion.div>
{/* Footer: active tab info */}
<div className="border-t border-border px-6 py-4 flex items-center gap-8 justify-between bg-surface-75">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="flex flex-col gap-1"
>
<h4 className="text-sm font-medium text-foreground">{active.label}</h4>
<p className="text-sm text-foreground-lighter">{active.paragraph}</p>
</motion.div>
</AnimatePresence>
<Link
href="/docs/guides/functions"
className="flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap shrink-0"
>
Documentation
<svg
width={12}
height={12}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0"
>
<path
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,157 @@
'use client'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useState } from 'react'
const LocalDXImage = dynamic(() => import('~/components/Products/Functions/LocalDXImage'))
const ParityImage = dynamic(() => import('~/components/Products/Functions/ParityImage'))
const NpmEcosystem = dynamic(() => import('~/components/Products/Functions/NpmEcosystem'))
const CI = dynamic(() => import('~/components/Products/Functions/CI'))
function EcosystemCard() {
const [isHovered, setIsHovered] = useState(false)
return (
<div
className="flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden col-span-1 md:row-span-2"
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-2 px-6 py-5">
<h4 className="text-foreground text-base font-medium">Use any NPM module</h4>
<p className="text-foreground-lighter text-sm">Tap into the Deno and NPM ecosystem</p>
</div>
</div>
)
}
function ParityCard() {
const [isHovered, setIsHovered] = useState(false)
return (
<div
className="flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden "
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-2 px-6 py-5">
<h4 className="text-foreground text-base 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="flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden md:col-span-2"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className="flex-1 flex items-center justify-center overflow-hidden pl-2 pt-8 md:pt-12"
style={{
maskImage: 'linear-gradient(to bottom, black 95%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 95%, transparent 100%)',
}}
>
<CI isHovered={isHovered} />
</div>
<div className="flex flex-col gap-2 px-6 py-5">
<h4 className="text-foreground text-base 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="flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden "
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-2 px-6 py-5">
<h4 className="text-foreground text-base 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 className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<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>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Edge Functions are developed using{' '}
<Link
href="https://deno.com/"
className="underline hover:text-foreground-light transition-colors"
target="_blank"
>
Deno
</Link>
, an open source JavaScript runtime that ensures maximum power and flexibility. Migrate
in and out at any time with no vendor lock-in.
</p>
</div>
</div>
{/* Bento grid */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 grid-flow-dense gap-3">
<LocalDXCard />
<ParityCard />
<CICard />
<EcosystemCard />
</div>
</div>
</div>
)
}
@@ -0,0 +1,86 @@
'use client'
import dynamic from 'next/dynamic'
import { useState } from 'react'
import { cn } from 'ui'
const RealtimeLogs = dynamic(() => import('~/components/Products/Functions/RealtimeLogs'))
const QueryLogs = dynamic(() => import('~/components/Products/Functions/QueryLogs'))
const Metrics = dynamic(() => import('~/components/Products/Functions/Metrics'))
const cards = [
{
id: 'realtime-logs',
label: 'Realtime logs',
paragraph: 'Stream logs to the dashboard in realtime with rich metadata to help debugging',
render: (isActive: boolean) => <RealtimeLogs isActive={isActive} isInView />,
},
{
id: 'log-explorer',
label: 'Query Logs via Log explorer',
paragraph: 'Get deeper insights into function behavior by writing SQL queries on function logs',
render: (isActive: boolean) => <QueryLogs isActive={isActive} isInView />,
},
{
id: 'metrics',
label: 'Metrics',
paragraph: 'Dashboards show the health of your functions at all times',
render: (isActive: boolean) => <Metrics isActive={isActive} />,
},
]
function ObservabilityCard({
card,
className,
}: {
card: (typeof cards)[number]
className?: string
}) {
const [isHovered, setIsHovered] = useState(false)
return (
<div
className={cn(
'flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden ',
className
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className="relative flex-1 min-h-[285px] overflow-hidden">{card.render(isHovered)}</div>
<div className="flex flex-col gap-2 px-6 py-5">
<h4 className="text-foreground text-base font-medium">{card.label}</h4>
<p className="text-foreground-lighter text-sm">{card.paragraph}</p>
</div>
</div>
)
}
export function ObservabilitySection() {
return (
<div className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Debug and monitor with <br />
<span className="text-foreground">built-in observability</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Monitor, debug, and optimize your Edge Functions with realtime logs, queryable log
explorer, and health dashboards — all built into the Supabase Dashboard.
</p>
</div>
</div>
{/* 3-column grid */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{cards.map((card) => (
<ObservabilityCard key={card.id} card={card} />
))}
</div>
</div>
</div>
)
}
@@ -0,0 +1,12 @@
import type { Metadata } from 'next'
import { EdgeFunctionsContent } from './_components/EdgeFunctionsContent'
export const metadata: Metadata = {
title: 'Edge Functions | Supabase',
description: 'Execute your code closest to your users with fast deploy times and low latency.',
}
export default function EdgeFunctionsPage() {
return <EdgeFunctionsContent />
}
+19
View File
@@ -0,0 +1,19 @@
'use client'
import Footer from '~/components/Footer'
import Nav from '~/components/Nav'
import { useForceDeepDark } from 'lib/theme.utils'
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
useForceDeepDark()
return (
<>
<Nav hideNavbar={false} />
<div className="relative w-full [--container-max-w:75rem]">
<main className="relative min-h-screen">{children}</main>
</div>
<Footer />
</>
)
}
@@ -0,0 +1,169 @@
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { ApiSectionClient } from './ApiSectionClient'
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#bda4ff' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#3ecf8e' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#3ecf8e' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#3ecf8e' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#ffffff' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
],
}
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#6b35dc' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#15593b' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#15593b' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#15593b' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#525252' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
],
}
const API_EXAMPLES = [
{
icon: 'Database' as const,
title: 'Database changes',
description: 'Listen to inserts, updates, and deletes in real time.',
code: `import { createClient } from '@supabase/supabase-js'
const supabase = createClient('URL', 'ANON_KEY')
const channel = supabase
.channel('db-changes')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'messages',
}, (payload) => {
console.log('New message:', payload.new)
})
.subscribe()`,
},
{
icon: 'Users' as const,
title: 'Presence',
description: 'Track and synchronize online user state across clients.',
code: `import { createClient } from '@supabase/supabase-js'
const supabase = createClient('URL', 'ANON_KEY')
const channel = supabase.channel('online-users')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({ user: 'user-123' })
}
})`,
},
{
icon: 'Radio' as const,
title: 'Broadcast',
description: 'Send ephemeral messages to all subscribed clients instantly.',
code: `import { createClient } from '@supabase/supabase-js'
const supabase = createClient('URL', 'ANON_KEY')
const channel = supabase.channel('cursor-pos')
channel
.on('broadcast', { event: 'cursor' }, (payload) => {
console.log('Cursor position:', payload)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.send({
type: 'broadcast',
event: 'cursor',
payload: { x: 100, y: 200 },
})
}
})`,
},
]
export async function ApiSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: ['javascript'],
})
const examples = API_EXAMPLES.map((example) => ({
icon: example.icon,
title: example.title,
description: example.description,
darkHtml: hl.codeToHtml(example.code, { lang: 'javascript', theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(example.code, { lang: 'javascript', theme: 'supabase-light' }),
}))
hl.dispose()
return <ApiSectionClient examples={examples} />
}
@@ -0,0 +1,200 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { Database, Radio, Users } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { Database, Users, Radio } as const
type IconName = keyof typeof ICONS
type ApiExample = {
icon: IconName
title: string
description: string
darkHtml: string
lightHtml: string
}
const INTERVAL_DURATION = 10000
export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = examples[activeIdx]
// Auto-size the code panel to the active snippet. We measure an always-mounted,
// invisible copy of the current code so the target height is known immediately on
// switch (and doesn't collapse during the cross-fade), then animate `height` to it.
const measureRef = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState<number>()
const hasMeasured = useRef(false)
useEffect(() => {
const el = measureRef.current
if (!el) return
const ro = new ResizeObserver(() => setHeight(el.offsetHeight))
ro.observe(el)
setHeight(el.offsetHeight)
return () => ro.disconnect()
}, [])
useEffect(() => {
if (height != null) hasMeasured.current = true
}, [height])
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % examples.length)
}
}, [progress, examples.length])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Simple and convenient
<br />
<span className="text-foreground">APIs</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
APIs that you can understand. With powerful libraries that work on client and
server-side applications.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tab cards */}
<div className="flex flex-col gap-1 items-start justify-end">
{examples.map((example, index) => {
const isActive = index === activeIdx
const Icon = ICONS[example.icon]
return (
<button
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{example.title}
</button>
)
})}
</div>
{/* Right: code block */}
<motion.div
initial={false}
animate={height != null ? { height } : undefined}
transition={
hasMeasured.current ? { duration: 0.35, ease: [0.22, 1, 0.36, 1] } : { duration: 0 }
}
className="relative self-end overflow-hidden border border-border rounded-md"
>
{/* Invisible measuring copy: drives the animated container height */}
<div
ref={measureRef}
aria-hidden
className="pointer-events-none invisible absolute inset-x-0 top-0 overflow-x-auto pb-12 [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
/>
<AnimatePresence mode="wait">
<motion.div
key={active.title}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="overflow-x-auto pb-12"
>
<div
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
className="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
<div
dangerouslySetInnerHTML={{ __html: active.lightHtml }}
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
<Link
href="/docs/guides/realtime"
className="absolute bottom-4 right-4 flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap"
>
Documentation
<svg
width={12}
height={12}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0"
>
<path
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</motion.div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,691 @@
'use client'
import { motion } from 'framer-motion'
import { Key } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { cn } from 'ui'
const FEATURES = [
{
title: 'Database changes',
description:
'Listen to inserts, updates, and deletes in your Postgres database and react instantly.',
visual: DatabaseChangesSkeleton,
},
{
title: 'Presence',
description:
'Store and synchronize online user state consistently across all connected clients.',
visual: PresenceSkeleton,
},
{
title: 'Broadcast',
description: 'Send any data to any client subscribed to the same channel with low latency.',
visual: BroadcastSkeleton,
},
]
type ChatMsg = { id: number; user: string; text: string }
const MAX_MESSAGES = 20
const initialMessages: ChatMsg[] = [
{ id: 1, user: 'Alice', text: 'Hey, is the deploy ready?' },
{ id: 2, user: 'Bob', text: 'Almost — running final tests now.' },
{ id: 3, user: 'Alice', text: 'Nice, let me know when it\u2019s live.' },
]
const incomingMessages: ChatMsg[] = [
{ id: 4, user: 'Bob', text: 'All green. Deploying now \u{1F680}' },
{ id: 5, user: 'Alice', text: 'Awesome, checking it out.' },
{ id: 6, user: 'Bob', text: 'Latency dropped to 42ms!' },
{ id: 7, user: 'Alice', text: 'That\u2019s a big improvement.' },
{ id: 8, user: 'Bob', text: 'Yeah, the new index helped.' },
{ id: 9, user: 'Alice', text: 'Should we update the docs?' },
{ id: 10, user: 'Bob', text: 'Already on it.' },
]
function DatabaseChangesSkeleton() {
const [messages, setMessages] = useState<ChatMsg[]>(initialMessages)
const [tableFlashId, setTableFlashId] = useState<number | null>(null)
const [chatFlashId, setChatFlashId] = useState<number | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const tableScrollRef = useRef<HTMLDivElement>(null)
const chatScrollRef = useRef<HTMLDivElement>(null)
const [isInView, setIsInView] = useState(false)
const nextMsgIdx = useRef(0)
useEffect(() => {
const el = containerRef.current
if (!el) return
const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), {
threshold: 0.3,
})
observer.observe(el)
return () => observer.disconnect()
}, [])
useEffect(() => {
if (!isInView) return
const innerTimers: ReturnType<typeof setTimeout>[] = []
const timer = setInterval(() => {
const msg = incomingMessages[nextMsgIdx.current % incomingMessages.length]
const newMsg = { ...msg, id: Date.now() + nextMsgIdx.current }
nextMsgIdx.current++
setMessages((prev) => [...prev.slice(-(MAX_MESSAGES - 1)), newMsg])
setTableFlashId(newMsg.id)
requestAnimationFrame(() => {
tableScrollRef.current?.scrollTo({
top: tableScrollRef.current.scrollHeight,
behavior: 'smooth',
})
})
const chatTimer = setTimeout(() => {
setChatFlashId(newMsg.id)
requestAnimationFrame(() => {
chatScrollRef.current?.scrollTo({
top: chatScrollRef.current.scrollHeight,
behavior: 'smooth',
})
})
const clearTimer = setTimeout(() => {
setTableFlashId(null)
setChatFlashId(null)
}, 600)
innerTimers.push(clearTimer)
}, 200)
innerTimers.push(chatTimer)
}, 3000)
return () => {
clearInterval(timer)
innerTimers.forEach(clearTimeout)
}
}, [isInView])
return (
<div
ref={containerRef}
className="relative w-full h-full flex flex-col justify-end"
style={{
maskImage:
'linear-gradient(to bottom, black 50%, rgba(0,0,0,0.9) 65%, rgba(0,0,0,0.5) 75%, rgba(0,0,0,0.1) 85%, transparent 90%)',
WebkitMaskImage:
'linear-gradient(to bottom, black 50%, rgba(0,0,0,0.9) 65%, rgba(0,0,0,0.5) 75%, rgba(0,0,0,0.1) 85%, transparent 90%)',
}}
>
<div
className="absolute top-12 -right-24 z-[2]"
style={{
maskImage: 'linear-gradient(to right, black 35%, transparent 70%)',
WebkitMaskImage: 'linear-gradient(to right, black 35%, transparent 70%)',
}}
>
<div
className="border border-border rounded-lg overflow-hidden bg-surface-75"
style={{ width: 420 }}
>
<table className="w-full border-collapse text-[13px] !mt-0 table-fixed">
<colgroup>
<col style={{ width: 72 }} />
<col style={{ width: 100 }} />
<col />
</colgroup>
<thead>
<tr className="bg-surface-200">
<th className="border-b border-r border-default px-3 py-1.5 text-left font-normal">
<div className="flex items-center gap-1.5 overflow-hidden">
<Key size={12} strokeWidth={2} className="text-brand rotate-45 shrink-0" />
<span className="text-foreground text-xs font-medium">id</span>
<span className="text-foreground-light text-xs">int8</span>
</div>
</th>
<th className="border-b border-r border-default px-3 py-1.5 text-left font-normal">
<div className="flex items-center gap-1.5 overflow-hidden">
<span className="text-foreground text-xs font-medium">user</span>
<span className="text-foreground-light text-xs">text</span>
</div>
</th>
<th className="border-b border-default px-3 py-1.5 text-left font-normal">
<div className="flex items-center gap-1.5 overflow-hidden">
<span className="text-foreground text-xs font-medium">text</span>
<span className="text-foreground-light text-xs">text</span>
</div>
</th>
</tr>
</thead>
</table>
<div
ref={tableScrollRef}
className="max-h-[58px] overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
<table className="w-full border-collapse text-sm !mt-0 table-fixed">
<colgroup>
<col style={{ width: 72 }} />
<col style={{ width: 100 }} />
<col />
</colgroup>
<tbody className="[&>tr:last-child>td]:border-b-0 [&>tr:first-child>td]:border-t-0">
{messages.map((msg) => (
<tr
key={msg.id}
className={cn(
'transition-colors duration-500',
tableFlashId === msg.id ? 'bg-brand/15 dark:bg-brand/5' : 'bg-surface-75'
)}
>
<td className="border-b border-r border-default px-3 py-1.5 text-foreground-muted text-xs truncate max-w-0">
{msg.id}
</td>
<td className="border-b border-r border-default px-3 py-1.5 text-foreground text-xs truncate max-w-0">
{msg.user}
</td>
<td className="border-b border-default px-3 py-1.5 text-foreground text-xs truncate max-w-0">
{msg.text}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Connecting dashed lines */}
<div className="absolute left-1/2 -translate-x-1/2 top-[140px] flex gap-2 z-[1] text-background-surface-300">
<svg width="2" height="52" fill="none" className="overflow-visible">
<line
x1="1"
y1="0"
x2="1"
y2="52"
stroke="currentColor"
strokeWidth="1.5"
strokeDasharray="4 4"
style={{ animation: 'rlDashFlow 1s linear infinite' }}
/>
</svg>
<svg width="2" height="52" fill="none" className="overflow-visible">
<line
x1="1"
y1="0"
x2="1"
y2="52"
stroke="currentColor"
strokeWidth="1.5"
strokeDasharray="4 4"
style={{ animation: 'rlDashFlow 1s linear infinite 0.5s' }}
/>
</svg>
<style>{`@keyframes rlDashFlow { to { stroke-dashoffset: -8; } }`}</style>
</div>
{/* Chat UI */}
<div className="px-4 pb-4 relative z-[2]">
<div className="border border-border rounded-lg overflow-hidden bg-surface-200 max-w-[90%]">
<div
ref={chatScrollRef}
className="h-[120px] overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden flex flex-col gap-2 px-3 py-3"
style={{
maskImage: 'linear-gradient(to bottom, transparent 0%, black 20%)',
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 20%)',
}}
>
{messages.map((msg) => {
const isAlice = msg.user === 'Alice'
return (
<div key={msg.id} className={cn('flex', isAlice ? 'justify-end' : 'justify-start')}>
<div
className={cn(
'max-w-[75%] px-3 py-1.5 rounded-xl text-xs leading-snug transition-colors duration-500',
isAlice
? 'bg-brand/15 text-foreground rounded-br-sm'
: 'bg-surface-300 text-foreground rounded-bl-sm',
chatFlashId === msg.id && 'ring-1 ring-brand/30'
)}
>
{msg.text}
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
)
}
const PRESENCE_USERS = [
{ name: 'Alan', initials: 'AD', color: '#3ECF8E' },
{ name: 'Jonny', initials: 'JW', color: '#6c63ff' },
{ name: 'Copple', initials: 'CP', color: '#F06A50' },
{ name: 'Terry', initials: 'TP', color: '#f1a10d' },
]
// Subtle looping paths — small movements around a home position
const CURSOR_PATHS: { x: number; y: number }[][] = [
[
{ x: 24, y: 38 },
{ x: 28, y: 42 },
{ x: 22, y: 44 },
{ x: 26, y: 36 },
],
[
{ x: 58, y: 30 },
{ x: 54, y: 34 },
{ x: 60, y: 36 },
{ x: 56, y: 28 },
],
[
{ x: 36, y: 64 },
{ x: 40, y: 68 },
{ x: 34, y: 70 },
{ x: 38, y: 62 },
],
[
{ x: 68, y: 56 },
{ x: 64, y: 60 },
{ x: 70, y: 62 },
{ x: 66, y: 54 },
],
]
// Different loop durations so cursors drift out of phase
const CURSOR_DURATIONS = [6, 7.5, 8.5, 7]
function PresenceCursor({
user,
path,
duration,
}: {
user: (typeof PRESENCE_USERS)[number]
path: { x: number; y: number }[]
duration: number
}) {
// Build looping keyframes — close the loop by appending the first point
const xFrames = [...path.map((p) => `${p.x}%`), `${path[0].x}%`]
const yFrames = [...path.map((p) => `${p.y}%`), `${path[0].y}%`]
return (
<motion.div
className="absolute"
initial={{ left: `${path[0].x}%`, top: `${path[0].y}%` }}
animate={{ left: xFrames, top: yFrames }}
transition={{
duration,
repeat: Infinity,
ease: 'easeInOut',
}}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
className="drop-shadow-md"
style={{ filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.25))' }}
aria-hidden
>
<path
d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"
fill={user.color}
stroke="white"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
<span
className="ml-3 -mt-0.5 inline-block rounded px-1.5 py-0.5 text-[9px] font-medium text-white shadow-sm"
style={{ backgroundColor: user.color }}
>
{user.name}
</span>
</motion.div>
)
}
const YOU_COLOR = '#e563f5'
function PresenceSkeleton() {
const containerRef = useRef<HTMLDivElement>(null)
const [hovered, setHovered] = useState(false)
const [mouse, setMouse] = useState({ x: 0, y: 0 })
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect()
setMouse({ x: e.clientX - rect.left, y: e.clientY - rect.top })
}
return (
<div
ref={containerRef}
className="relative h-full w-full overflow-hidden"
style={{ cursor: hovered ? 'none' : undefined }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onMouseMove={handleMouseMove}
>
{/* Stacked avatars — top right */}
<div className="absolute right-4 top-4 z-10 flex -space-x-2">
{PRESENCE_USERS.map((user) => (
<div
key={user.name}
className="flex h-7 w-7 items-center justify-center rounded-full bg-surface-200 text-[9px] font-medium text-foreground"
style={{
boxShadow: `0 0 0 2px hsl(var(--background-surface-75)), 0 0 0 4px ${user.color}`,
}}
>
{user.initials}
</div>
))}
{/* Your avatar — appears on hover */}
<motion.div
className="flex h-7 w-7 items-center justify-center rounded-full bg-surface-200 text-[9px] font-medium text-foreground"
style={{
boxShadow: `0 0 0 2px hsl(var(--background-surface-75)), 0 0 0 4px ${YOU_COLOR}`,
}}
initial={{ opacity: 0, scale: 0.5, width: 0, marginLeft: 0 }}
animate={
hovered
? { opacity: 1, scale: 1, width: 28, marginLeft: -8 }
: { opacity: 0, scale: 0.5, width: 0, marginLeft: 0 }
}
transition={{ type: 'spring', duration: 0.47, bounce: 0 }}
>
You
</motion.div>
</div>
{/* Animated cursors */}
{PRESENCE_USERS.map((user, i) => (
<PresenceCursor
key={user.name}
user={user}
path={CURSOR_PATHS[i]}
duration={CURSOR_DURATIONS[i]}
/>
))}
{/* Your cursor — follows mouse on hover */}
{hovered && (
<div className="pointer-events-none absolute z-20" style={{ left: mouse.x, top: mouse.y }}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
className="drop-shadow-md"
style={{ filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.25))' }}
aria-hidden
>
<path
d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"
fill={YOU_COLOR}
stroke="white"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
<span
className="ml-3 -mt-0.5 inline-block rounded px-1.5 py-0.5 text-[9px] font-medium text-white shadow-sm"
style={{ backgroundColor: YOU_COLOR }}
>
You
</span>
</div>
)}
</div>
)
}
const BROADCAST_PATHS = [
{ d: 'M180,58 C180,135 80,175 80,268', delay: 0 },
{ d: 'M180,58 C180,135 140,175 140,268', delay: 0.15 },
{ d: 'M180,58 C180,135 220,175 220,268', delay: 0.3 },
{ d: 'M180,58 C180,135 280,175 280,268', delay: 0.45 },
]
const BROADCAST_CLIENTS = [
{
// User icon
icon: (
<>
<path
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<circle cx="12" cy="7" r="4" stroke="currentColor" strokeWidth="1.5" />
</>
),
},
{
// Image icon
icon: (
<>
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="1.5" />
<circle cx="9" cy="9" r="2" stroke="currentColor" strokeWidth="1.5" />
<path
d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</>
),
},
{
// Layers icon
icon: (
<>
<path
d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="m2 12 8.58 3.91a2 2 0 0 0 1.66 0L21 12"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="m2 17 8.58 3.91a2 2 0 0 0 1.66 0L21 17"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
),
},
{
// Database icon
icon: (
<>
<ellipse cx="12" cy="5" rx="7" ry="3" stroke="currentColor" strokeWidth="1.5" />
<path d="M19 12c0 1.66-3.13 3-7 3s-7-1.34-7-3" stroke="currentColor" strokeWidth="1.5" />
<path d="M5 5v14c0 1.66 3.13 3 7 3s7-1.34 7-3V5" stroke="currentColor" strokeWidth="1.5" />
</>
),
},
]
const PULSE_DURATION = 3000
// Time (ms) for pulse leading edge to reach the node
const PULSE_ARRIVAL = 350
function BroadcastSkeleton() {
const [flashSet, setFlashSet] = useState<Set<number>>(new Set())
useEffect(() => {
const timers: ReturnType<typeof setTimeout>[] = []
function scheduleFlashes() {
BROADCAST_PATHS.forEach((line, i) => {
const arrivalOffset = line.delay * 1000 + PULSE_ARRIVAL
const t = setTimeout(() => {
setFlashSet((prev) => new Set(prev).add(i))
const clearT = setTimeout(() => {
setFlashSet((prev) => {
const next = new Set(prev)
next.delete(i)
return next
})
}, 500)
timers.push(clearT)
}, arrivalOffset)
timers.push(t)
})
}
scheduleFlashes()
const interval = setInterval(scheduleFlashes, PULSE_DURATION)
return () => {
timers.forEach(clearTimeout)
clearInterval(interval)
}
}, [])
return (
<div className="relative h-full w-full">
{/* Source node — top center */}
<div className="absolute left-1/2 top-5 z-10 -translate-x-1/2">
<div className="flex items-center gap-2 rounded-xl border border-border bg-surface-100 px-3 py-2 shadow-xs dark:shadow-sm">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
className="text-brand"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="m22 8-6 4 6 4V8Z" stroke="currentColor" strokeWidth="1.5" />
<rect
x="2"
y="6"
width="14"
height="12"
rx="2"
stroke="currentColor"
strokeWidth="1.5"
/>
</svg>
<span className="text-xs font-medium text-foreground">channel:room-1</span>
</div>
</div>
{/* SVG curves */}
<svg
className="absolute inset-0 h-full w-full"
viewBox="0 0 360 320"
fill="none"
preserveAspectRatio="xMidYMid meet"
>
{BROADCAST_PATHS.map((line, i) => (
<g key={i}>
<path
d={line.d}
stroke="hsl(var(--foreground-muted))"
strokeWidth="2"
strokeLinecap="round"
opacity="0.15"
/>
<path
d={line.d}
stroke="#3ECF8E"
strokeWidth="2"
strokeLinecap="round"
strokeDasharray="40 560"
style={{
animation: `broadcastPulse 3s ${line.delay}s linear infinite`,
}}
/>
</g>
))}
</svg>
{/* Destination nodes — positioned to match SVG path endpoints */}
{BROADCAST_CLIENTS.map((client, i) => {
const xPercent = [23.2, 39.5, 61, 77][i]
const isFlashing = flashSet.has(i)
return (
<div
key={i}
className={cn(
'absolute bottom-6 z-10 flex h-10 w-10 -translate-x-1/2 items-center justify-center rounded-xl border bg-surface-100 shadow-xs dark:shadow-sm transition-all duration-300',
isFlashing ? 'border-brand text-brand' : 'border-border text-foreground-muted'
)}
style={{ left: `${xPercent}%` }}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden>
{client.icon}
</svg>
</div>
)
})}
<style>{`
@keyframes broadcastPulse {
from { stroke-dashoffset: 600; }
to { stroke-dashoffset: 0; }
}
`}</style>
</div>
)
}
export function FeaturesSection() {
return (
<div className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Three ways
<br />
<span className="text-foreground">to go realtime</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Database changes, presence tracking, and broadcast messaging — everything you need to
build collaborative, real-time applications.
</p>
</div>
</div>
{/* 3-col grid */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{FEATURES.map((feature) => {
const Visual = feature.visual
return (
<div
key={feature.title}
className="flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden"
>
<div className="relative flex items-center justify-center h-[320px]">
<Visual />
</div>
<div className="px-6 py-5 flex flex-col gap-1">
<h4 className="text-foreground text-sm font-medium">{feature.title}</h4>
<p className="text-foreground-lighter text-sm">{feature.description}</p>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
@@ -0,0 +1,29 @@
import Link from 'next/link'
import { Button } from 'ui'
export function Hero() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-32 pb-16 md:pt-40 md:pb-32">
<div className="flex flex-col gap-6 lg:gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h1 className="text-foreground text-3xl sm:text-5xl sm:leading-none">
<span className="block">Realtime</span>
<span className="text-foreground-lighter block">sync in milliseconds</span>
</h1>
<p className="text-foreground-lighter text-sm lg:text-base">
Sync client state globally over WebSockets. Listen to database changes, store user
presence, and broadcast messages to all connected clients.
</p>
</div>
<div className="flex items-center gap-2">
<Button asChild size="medium">
<Link href="https://supabase.com/dashboard">Start your project</Link>
</Button>
<Button asChild size="medium" type="default">
<Link href="/docs/guides/realtime">Documentation</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,15 @@
import { QuoteSection as SharedQuoteSection } from '../../_shared/QuoteSection'
export function QuoteSection() {
return (
<SharedQuoteSection
quote="Supabase takes out the mental effort from our back-end infrastructure so we can focus on our customers needs."
author={{
name: 'Aaron Sullivan',
role: 'Principal Software Engineer at Epsilon3',
image: '/images/blog/avatars/aaron-epsilon3.png',
link: '/customers/epsilon3',
}}
/>
)
}
@@ -0,0 +1,28 @@
import { BuiltWithSupabaseSection } from '~/app/(home)/_components/BuiltWithSupabaseSection'
import { CTASection } from '~/app/(home)/_components/CTASection'
import { FeaturesSection } from './FeaturesSection'
import { Hero } from './Hero'
import { QuoteSection } from './QuoteSection'
export function RealtimeContent({ apiSlot }: { apiSlot: React.ReactNode }) {
return (
<div className="overflow-x-clip">
<section>
<Hero />
</section>
<section className="border-t border-border">
<FeaturesSection />
</section>
<section className="border-t border-border">
<QuoteSection />
</section>
<section className="border-t border-border">{apiSlot}</section>
<section className="border-t border-border">
<BuiltWithSupabaseSection />
</section>
<section className="border-t border-border">
<CTASection />
</section>
</div>
)
}
+14
View File
@@ -0,0 +1,14 @@
import type { Metadata } from 'next'
import { ApiSection } from './_components/ApiSection'
import { RealtimeContent } from './_components/RealtimeContent'
export const metadata: Metadata = {
title: 'Realtime | Supabase',
description:
'Sync client state globally over WebSockets. Listen to database changes, store user presence, and broadcast messages in real time.',
}
export default function RealtimePage() {
return <RealtimeContent apiSlot={<ApiSection />} />
}
@@ -0,0 +1,257 @@
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { ApiSectionClient } from './ApiSectionClient'
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#bda4ff' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#3ecf8e' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#3ecf8e' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#3ecf8e' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#ffffff' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
],
}
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#6b35dc' },
},
{
scope: [
'entity.name.function',
'support.function',
'entity.name.tag',
'support.class.component',
],
settings: { foreground: '#15593b' },
},
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#15593b' },
},
{
scope: [
'variable.other.property',
'support.type.property-name',
'meta.object-literal.key',
'entity.other.attribute-name',
],
settings: { foreground: '#15593b' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#525252' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
],
}
const LANG_MAP: Record<string, string> = {
JavaScript: 'javascript',
Flutter: 'dart',
Python: 'python',
'C#': 'csharp',
Kotlin: 'kotlin',
Swift: 'swift',
}
const API_EXAMPLES = [
{
icon: 'Upload' as const,
title: 'Upload a file',
description: 'Upload any file type to your storage buckets with a single call.',
code: {
JavaScript: `// Upload an image to the "avatars" bucket
const spaceCat = event.target.files[0]
const { data, error } = await supabase
.storage
.from('avatars')
.upload('space-cat.png', spaceCat)`,
Flutter: `// Upload an image to the "avatars" bucket
final file = File('space-cat.png');
await supabase
.storage
.from('avatars')
.upload('space-cat.png', file);`,
Python: `# Upload an image to the "avatars" bucket
with open("space-cat.png", "rb") as f:
supabase.storage.from_("avatars").upload(
"space-cat.png", f
)`,
'C#': `// Upload an image to the "avatars" bucket
var bytes = File.ReadAllBytes("space-cat.png");
await supabase.Storage
.From("avatars")
.Upload(bytes, "space-cat.png");`,
Kotlin: `// Upload an image to the "avatars" bucket
supabase.storage.from("avatars")
.upload("space-cat.png", file)`,
Swift: `// Upload an image to the "avatars" bucket
let data = try Data(contentsOf: fileURL)
try await supabase.storage
.from("avatars")
.upload("space-cat.png", data: data)`,
},
},
{
icon: 'Download' as const,
title: 'Download a file',
description: 'Retrieve files from your buckets directly or via signed URLs.',
code: {
JavaScript: `// Download the "space-cat.png" from "avatars"
const { data, error } = await supabase
.storage
.from('avatars')
.download('space-cat.png')`,
Flutter: `// Download the "space-cat.png" from "avatars"
final bytes = await supabase
.storage
.from('avatars')
.download('space-cat.png');`,
Python: `# Download the "space-cat.png" from "avatars"
data = supabase.storage.from_("avatars") \\
.download("space-cat.png")`,
'C#': `// Download the "space-cat.png" from "avatars"
var bytes = await supabase.Storage
.From("avatars")
.Download("space-cat.png");`,
Kotlin: `// Download the "space-cat.png" from "avatars"
val bytes = supabase.storage.from("avatars")
.downloadAuthenticated("space-cat.png")`,
Swift: `// Download the "space-cat.png" from "avatars"
let data = try await supabase.storage
.from("avatars")
.download(path: "space-cat.png")`,
},
},
{
icon: 'List' as const,
title: 'List files',
description: 'Browse and list all files within a storage bucket.',
code: {
JavaScript: `// List all the files in the "avatars" bucket
const { data, error } = await supabase
.storage
.from('avatars')
.list()`,
Flutter: `// List all the files in the "avatars" bucket
final files = await supabase
.storage
.from('avatars')
.list();`,
Python: `# List all the files in the "avatars" bucket
files = supabase.storage.from_("avatars") \\
.list()`,
'C#': `// List all the files in the "avatars" bucket
var files = await supabase.Storage
.From("avatars")
.List();`,
Kotlin: `// List all the files in the "avatars" bucket
val files = supabase.storage.from("avatars")
.list()`,
Swift: `// List all the files in the "avatars" bucket
let files = try await supabase.storage
.from("avatars")
.list()`,
},
},
{
icon: 'ArrowRightLeft' as const,
title: 'Move and rename',
description: 'Organize your files by moving and renaming within or across buckets.',
code: {
JavaScript: `// Move and rename files
const { data, error } = await supabase
.storage
.from('avatars')
.move('public/space-cat.png', 'private/space-cat.png')`,
Flutter: `// Move and rename files
await supabase
.storage
.from('avatars')
.move('public/space-cat.png', 'private/space-cat.png');`,
Python: `# Move and rename files
supabase.storage.from_("avatars") \\
.move("public/space-cat.png", "private/space-cat.png")`,
'C#': `// Move and rename files
await supabase.Storage
.From("avatars")
.Move("public/space-cat.png", "private/space-cat.png");`,
Kotlin: `// Move and rename files
supabase.storage.from("avatars")
.move("public/space-cat.png", "private/space-cat.png")`,
Swift: `// Move and rename files
try await supabase.storage
.from("avatars")
.move(
from: "public/space-cat.png",
to: "private/space-cat.png"
)`,
},
},
]
export async function ApiSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: Object.values(LANG_MAP),
})
const examples = API_EXAMPLES.map((example) => ({
icon: example.icon,
title: example.title,
description: example.description,
languages: Object.fromEntries(
Object.entries(example.code).map(([lang, code]) => [
lang,
{
darkHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-light' }),
},
])
),
}))
hl.dispose()
return <ApiSectionClient examples={examples} />
}
@@ -0,0 +1,222 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { ArrowRightLeft, ChevronDown, Download, List, Upload } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { Upload, Download, List, ArrowRightLeft } as const
type IconName = keyof typeof ICONS
type ApiExample = {
icon: IconName
title: string
description: string
languages: Record<string, { darkHtml: string; lightHtml: string }>
}
const LANGUAGES = ['JavaScript', 'Flutter', 'Python', 'C#', 'Kotlin', 'Swift']
const INTERVAL_DURATION = 8000
export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [activeLang, setActiveLang] = useState('JavaScript')
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = examples[activeIdx]
// Auto-size the code panel to the active snippet. We measure an always-mounted,
// invisible copy of the current code so the target height is known immediately on
// switch (and doesn't collapse during the cross-fade), then animate `height` to it.
const measureRef = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState<number>()
const hasMeasured = useRef(false)
useEffect(() => {
const el = measureRef.current
if (!el) return
const ro = new ResizeObserver(() => setHeight(el.offsetHeight))
ro.observe(el)
setHeight(el.offsetHeight)
return () => ro.disconnect()
}, [])
useEffect(() => {
if (height != null) hasMeasured.current = true
}, [height])
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % examples.length)
}
}, [progress, examples.length])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Simple and convenient
<br />
<span className="text-foreground">APIs</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Built from the ground up for interoperable authentication. Fast and easy to implement
using our powerful library clients.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tab cards */}
<div className="flex flex-col gap-1 items-start justify-end">
{examples.map((example, index) => {
const isActive = index === activeIdx
const Icon = ICONS[example.icon]
return (
<button
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{example.title}
</button>
)
})}
</div>
{/* Right: code block */}
<motion.div
initial={false}
animate={height != null ? { height } : undefined}
transition={
hasMeasured.current ? { duration: 0.35, ease: [0.22, 1, 0.36, 1] } : { duration: 0 }
}
className="relative self-end overflow-hidden border border-border rounded-md"
>
{/* Invisible measuring copy: drives the animated container height */}
<div
ref={measureRef}
aria-hidden
className="pointer-events-none invisible absolute inset-x-0 top-0 overflow-x-auto pb-12 [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: active.languages[activeLang]?.darkHtml ?? '' }}
/>
{/* Language switcher */}
<div className="absolute top-3 right-4 z-10 flex items-center">
<div className="relative flex items-center gap-1 text-foreground-muted text-xs">
<select
value={activeLang}
onChange={(e) => setActiveLang(e.target.value)}
className="appearance-none bg-transparent text-foreground-light text-xs pr-4 cursor-pointer border-none outline-none shadow-none focus:border-none focus:outline-none focus:shadow-none focus:ring-0"
>
{LANGUAGES.map((lang) => (
<option key={lang} value={lang}>
{lang}
</option>
))}
</select>
<ChevronDown size={12} className="absolute right-0 pointer-events-none" />
</div>
</div>
<AnimatePresence mode="wait">
<motion.div
key={`${active.title}-${activeLang}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="overflow-x-auto pb-12"
>
<div
dangerouslySetInnerHTML={{ __html: active.languages[activeLang]?.darkHtml ?? '' }}
className="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
<div
dangerouslySetInnerHTML={{
__html: active.languages[activeLang]?.lightHtml ?? '',
}}
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
<Link
href="/docs/guides/storage"
className="absolute bottom-4 right-4 flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap"
>
Documentation
<svg
width={12}
height={12}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0"
>
<path
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</motion.div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,81 @@
import { Check } from 'lucide-react'
import Link from 'next/link'
import { Button } from 'ui'
const BUCKET_TYPES = [
{
title: 'Files buckets',
features: [
'Store images, videos, documents, and more',
'Serve fast from a global CDN',
'Fine-grained access controls',
'Image optimizations and transformations',
],
cta: { label: 'Learn more', href: '/docs/guides/storage' },
},
{
title: 'Analytics buckets',
features: [
'Apache Iceberg open table format',
'Efficient query, partition, and transform',
'Historical and time-series data',
'Optionally expose via Postgres',
],
cta: { label: 'Learn more', href: '/docs/guides/storage/analytics-buckets' },
},
{
title: 'Vector buckets',
features: [
'Store and index vector embeddings',
'Multiple distance metrics',
'Metadata filtering and similarity queries',
'RAG systems and AI-powered search',
],
cta: { label: 'Learn more', href: '/docs/guides/storage/vector-buckets' },
},
]
export function BucketTypesSection() {
return (
<div className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Bucket types
<br />
<span className="text-foreground">for every application</span>
</h3>
</div>
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
{BUCKET_TYPES.map((bucket) => (
<div
key={bucket.title}
className="relative flex flex-col overflow-hidden min-h-[320px] bg-surface-75 border border-border rounded-lg"
>
<div className="relative z-10 flex flex-col justify-between h-full p-6 md:p-8">
<div className="flex flex-col gap-4">
<h4 className="text-foreground text-lg font-medium">{bucket.title}</h4>
<ul className="flex flex-col text-foreground-lighter text-sm gap-1.5">
{bucket.features.map((feature) => (
<li key={feature} className="flex items-start gap-2">
<Check className="w-3.5 h-3.5 mt-0.5 shrink-0 stroke-2 text-brand" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
<div className="mt-6">
<Button type="default" size="small" asChild>
<Link href={bucket.cta.href}>{bucket.cta.label}</Link>
</Button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
@@ -0,0 +1,331 @@
'use client'
import { cn } from 'ui'
const FEATURES = [
{
title: 'Interoperable',
description: 'Integrates with the rest of Supabase — Auth and Postgres work together.',
visual: InteroperableSkeleton,
},
{
title: 'Lightning fast',
description:
'Thin API server layer built on Postgres permissions with content delivered via global CDN.',
visual: CDNSkeleton,
},
{
title: 'Multiple bucket types',
description:
'Files, Analytics, or Vector buckets — choose the right storage model for your application.',
visual: BucketsSkeleton,
},
]
function InteroperableSkeleton() {
return (
<div className="relative flex h-full w-full items-center justify-center overflow-hidden [mask-image:radial-gradient(ellipse_88%_88%_at_50%_50%,black_40%,transparent_80%)] [webkit-mask-image:radial-gradient(ellipse_88%_88%_at_50%_50%,black_40%,transparent_80%)]">
<div className="relative h-[220px] w-[310px]">
<div className="pointer-events-none absolute inset-0 text-foreground-muted/12">
<line x1="155" y1="8" x2="155" y2="212" className="stroke-current" strokeWidth="1" />
<line x1="60" y1="8" x2="60" y2="212" className="stroke-current" strokeWidth="1" />
<line x1="250" y1="8" x2="250" y2="212" className="stroke-current" strokeWidth="1" />
<line x1="8" y1="110" x2="302" y2="110" className="stroke-current" strokeWidth="1" />
</div>
<div className="pointer-events-none absolute left-[58px] top-1/2 flex w-[56px] -translate-y-1/2 flex-col gap-1">
{[0, 1].map((i) => (
<div
key={`left-conn-${i}`}
className="relative h-[1.5px] w-full overflow-hidden bg-brand/18 [mask-image:linear-gradient(to_right,transparent,black_20%,black_80%,transparent)] [webkit-mask-image:linear-gradient(to_right,transparent,black_20%,black_80%,transparent)]"
>
<div
className="absolute inset-y-0 left-0 w-24 [animation:interopLineSweep_1.05s_linear_infinite]"
style={{
animationDelay: `${i * 100}ms`,
background:
'linear-gradient(to right, hsl(var(--brand-default)/0), hsl(var(--brand-default)/0.95), hsl(var(--brand-default)/0))',
}}
/>
</div>
))}
</div>
<div className="pointer-events-none absolute right-[58px] top-1/2 flex w-[56px] -translate-y-1/2 flex-col gap-1">
{[0, 1].map((i) => (
<div
key={`right-conn-${i}`}
className="relative h-[1.5px] w-full overflow-hidden bg-brand/18 [mask-image:linear-gradient(to_right,transparent,black_20%,black_80%,transparent)] [webkit-mask-image:linear-gradient(to_right,transparent,black_20%,black_80%,transparent)]"
>
<div
className="absolute inset-y-0 left-0 w-24 [animation:interopLineSweep_1.05s_linear_infinite]"
style={{
animationDelay: `${140 + i * 100}ms`,
background:
'linear-gradient(to right, hsl(var(--brand-default)/0), hsl(var(--brand-default)/0.95), hsl(var(--brand-default)/0))',
}}
/>
</div>
))}
</div>
<div className="absolute left-0 top-1/2 flex h-[70px] w-[70px] -translate-y-1/2 items-center justify-center rounded-xl border border-border bg-surface-100/95 shadow-[0_2px_10px_hsl(var(--background-default)/0.15)]">
<div className="absolute inset-3 rounded-full border border-foreground-muted/15" />
<div className="absolute inset-5 rounded-full border border-foreground-muted/10" />
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
className="relative z-10 text-foreground-muted"
strokeWidth="1.9"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" className="stroke-current" />
<circle cx="12" cy="7" r="4" className="stroke-current" />
</svg>
</div>
<div className="absolute left-1/2 top-1/2 flex h-[92px] w-[108px] -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-2xl border border-border bg-surface-100/95 shadow-[0_6px_18px_hsl(var(--background-default)/0.16)]">
<div className="absolute inset-3 rounded-[14px] border border-foreground-muted/10" />
<div className="absolute size-12 rounded-full border border-foreground-muted/15" />
<div className="absolute size-16 rounded-full border border-foreground-muted/8" />
<svg
width="34"
height="34"
viewBox="0 0 24 24"
fill="none"
className="relative z-10 text-brand"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
className="stroke-current"
/>
</svg>
</div>
<div className="absolute right-0 top-1/2 flex h-[70px] w-[70px] -translate-y-1/2 items-center justify-center rounded-xl border border-border bg-surface-100/95 shadow-[0_2px_10px_hsl(var(--background-default)/0.15)]">
<div className="absolute inset-3 rounded-full border border-foreground-muted/15" />
<div className="absolute inset-5 rounded-full border border-foreground-muted/10" />
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
className="relative z-10 text-foreground-muted"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<ellipse cx="12" cy="5" rx="7" ry="3" className="stroke-current" />
<path d="M19 12c0 1.66-3.13 3-7 3s-7-1.34-7-3" className="stroke-current" />
<path d="M5 5v14c0 1.66 3.13 3 7 3s7-1.34 7-3V5" className="stroke-current" />
</svg>
</div>
<style>{`
@keyframes interopLineSweep {
from {
transform: translateX(-120%);
}
to {
transform: translateX(260%);
}
}
`}</style>
</div>
</div>
)
}
function CDNSkeleton() {
return (
<div className="relative flex h-full w-full items-center justify-center [mask-image:radial-gradient(ellipse_85%_85%_at_50%_50%,black_35%,transparent_78%)] [webkit-mask-image:radial-gradient(ellipse_85%_85%_at_50%_50%,black_35%,transparent_78%)]">
<div className="absolute inset-0 z-[1] flex items-center justify-center">
<div className="flex w-[290px] flex-col gap-0.5">
{[-20, -10, 0, 10, 20].map((offset, i) => (
<div
key={`flash-${offset}`}
className="relative h-px w-full overflow-hidden bg-brand/15 [mask-image:linear-gradient(to_right,transparent,black_20%,black_80%,transparent)] [webkit-mask-image:linear-gradient(to_right,transparent,black_20%,black_80%,transparent)]"
style={{ transform: `translateY(${offset}px)` }}
>
<div
className="absolute inset-y-0 left-0 w-24 [animation:cdnFlashSweep_1.05s_linear_infinite]"
style={{
animationDelay: `${i * 130}ms`,
background:
'linear-gradient(to right, hsl(var(--brand-default)/0), hsl(var(--brand-default)/0.9), hsl(var(--brand-default)/0))',
}}
/>
</div>
))}
</div>
</div>
<div className="absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2">
<div className="pointer-events-none absolute left-1/2 top-1/2 -z-10 -translate-x-1/2 -translate-y-1/2">
<div className="absolute -left-[70px] top-1/2 h-[88px] w-[122px] -translate-y-1/2 rounded-[20px] border border-border bg-surface-100 shadow-[0_2px_10px_hsl(var(--background-default)/0.18)]" />
<div className="absolute -right-[70px] top-1/2 h-[88px] w-[122px] -translate-y-1/2 rounded-[20px] border border-border bg-surface-100 shadow-[0_2px_10px_hsl(var(--background-default)/0.18)]" />
</div>
<div className="relative flex h-[116px] w-[116px] items-center justify-center rounded-[24px] border border-foreground/15 bg-surface-200/95 shadow-[0_8px_28px_hsl(var(--background-default)/0.24)]">
<div className="absolute inset-0 rounded-[24px] shadow-[inset_0_1px_0_hsl(var(--foreground-default)/0.06)]" />
<div className="absolute size-10 rounded-full bg-brand/15 blur-xl" />
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
className="relative z-10 text-brand"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
className="stroke-current"
/>
</svg>
</div>
</div>
<style>{`
@keyframes cdnFlashSweep {
from {
transform: translateX(-120%);
}
to {
transform: translateX(260%);
}
}
`}</style>
</div>
)
}
function BucketsSkeleton() {
const buckets = [
{
name: 'Files',
label: 'images, videos, docs',
icon: 'folder' as const,
colorClass: 'text-brand',
},
{
name: 'Analytics',
label: 'Iceberg, time-series',
icon: 'chart' as const,
colorClass: 'text-blue-700 dark:text-blue-400',
},
{
name: 'Vectors',
label: 'embeddings, RAG',
icon: 'sparkles' as const,
colorClass: 'text-purple-700 dark:text-purple-400',
},
]
return (
<div className="flex items-center justify-center w-full h-full">
<div className="flex w-64 flex-col gap-2">
{buckets.map((bucket) => (
<div
key={bucket.name}
className="flex items-center justify-between px-4 py-2.5 rounded-lg border border-border bg-surface-200"
>
<div className="flex items-center gap-2">
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
className={cn('shrink-0', bucket.colorClass)}
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
{bucket.icon === 'folder' && (
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
className="stroke-current"
/>
)}
{bucket.icon === 'chart' && (
<>
<line x1="5" y1="20" x2="5" y2="10" className="stroke-current" />
<line x1="12" y1="20" x2="12" y2="6" className="stroke-current" />
<line x1="19" y1="20" x2="19" y2="13" className="stroke-current" />
</>
)}
{bucket.icon === 'sparkles' && (
<>
<path
d="M12 3l2.3 5.2L20 10.5l-5.7 2.3L12 18l-2.3-5.2L4 10.5l5.7-2.3L12 3z"
className="stroke-current"
/>
<path d="M19 3v4" className="stroke-current" />
<path d="M21 5h-4" className="stroke-current" />
</>
)}
</svg>
<span className={cn('text-xs font-medium', bucket.colorClass)}>{bucket.name}</span>
</div>
<span className="text-foreground-muted text-[11px]">{bucket.label}</span>
</div>
))}
</div>
</div>
)
}
export function FeaturesSection() {
return (
<div className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Everything you need
<br />
<span className="text-foreground">for storage</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
S3 compatible storage with global CDN, image transformations, and fine-grained access
controls powered by Postgres RLS.
</p>
</div>
</div>
{/* 3-col grid */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{FEATURES.map((feature) => {
const Visual = feature.visual
return (
<div
key={feature.title}
className="flex flex-col bg-surface-75 border border-border rounded-lg overflow-hidden"
>
<div className="relative flex items-center justify-center h-[320px]">
<Visual />
</div>
<div className="px-6 py-5 flex flex-col gap-1">
<h4 className="text-foreground text-sm font-medium">{feature.title}</h4>
<p className="text-foreground-lighter text-sm">{feature.description}</p>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
@@ -0,0 +1,29 @@
import Link from 'next/link'
import { Button } from 'ui'
export function Hero() {
return (
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-32 pb-16 md:pt-40 md:pb-32">
<div className="flex flex-col gap-6 lg:gap-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h1 className="text-foreground text-3xl sm:text-5xl sm:leading-none">
<span className="block">Store and serve</span>
<span className="text-foreground-lighter block">any type of digital content</span>
</h1>
<p className="text-foreground-lighter text-sm lg:text-base">
An open source S3 compatible storage solution with unlimited scalability, for any file
type. With custom policies and permissions that are familiar and easy to implement.
</p>
</div>
<div className="flex items-center gap-2">
<Button asChild size="medium">
<Link href="https://supabase.com/dashboard">Start your project</Link>
</Button>
<Button asChild size="medium" type="default">
<Link href="/docs/guides/storage">Documentation</Link>
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,111 @@
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { PermissionsSectionClient } from './PermissionsSectionClient'
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#bda4ff' },
},
{ scope: ['entity.name.function', 'support.function'], settings: { foreground: '#3ecf8e' } },
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#3ecf8e' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
],
}
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{
scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
settings: { foreground: '#6b35dc' },
},
{ scope: ['entity.name.function', 'support.function'], settings: { foreground: '#15593b' } },
{
scope: ['constant', 'variable.other.constant', 'support.constant'],
settings: { foreground: '#15593b' },
},
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
],
}
const PERMISSION_EXAMPLES = [
{
title: 'Public access to a bucket',
description: 'Allow any user access to all files in a bucket.',
code: `create policy "Public Access"
on storage.objects for all
using ( bucket_id = 'avatars' );`,
},
{
title: 'Public access to a folder',
description: 'Allow public CRUD access to a specific folder within a bucket.',
code: `create policy "Public access to a folder"
on storage.objects for all
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = 'public'
);`,
},
{
title: 'Authenticated access',
description: 'Restrict access to authenticated users for a specific folder.',
code: `create policy "Logged in access"
on storage.objects
for all using (
bucket_id = 'avatars'
and auth.role() = 'authenticated'
and (storage.foldername(name))[1] = 'authenticated'
);`,
},
{
title: 'Individual file access',
description: 'Grant a specific user access to a single file by UID.',
code: `create policy "Individual access"
on storage.objects for all
using (
bucket_id = 'avatars'
and name = 'folder/only_uid.jpg'
and (select auth.uid()) = 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2'
);`,
},
]
export async function PermissionsSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: ['sql'],
})
const examples = PERMISSION_EXAMPLES.map((example) => ({
title: example.title,
description: example.description,
darkHtml: hl.codeToHtml(example.code, { lang: 'sql', theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(example.code, { lang: 'sql', theme: 'supabase-light' }),
}))
hl.dispose()
return <PermissionsSectionClient examples={examples} />
}
@@ -0,0 +1,200 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
type PermissionExample = {
title: string
description: string
darkHtml: string
lightHtml: string
}
const INTERVAL_DURATION = 10000
export function PermissionsSectionClient({ examples }: { examples: PermissionExample[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = examples[activeIdx]
// Auto-size the code panel to the active snippet. We measure an always-mounted,
// invisible copy of the current code so the target height is known immediately on
// switch (and doesn't collapse during the cross-fade), then animate `height` to it.
const measureRef = useRef<HTMLDivElement>(null)
const [height, setHeight] = useState<number>()
const hasMeasured = useRef(false)
useEffect(() => {
const el = measureRef.current
if (!el) return
const ro = new ResizeObserver(() => setHeight(el.offsetHeight))
ro.observe(el)
setHeight(el.offsetHeight)
return () => ro.disconnect()
}, [])
useEffect(() => {
if (height != null) hasMeasured.current = true
}, [height])
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % examples.length)
}
}, [progress, examples.length])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Integrates natively
<br />
<span className="text-foreground">with Supabase Auth</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Storage authorization is built around Postgres so that you can use any combination of
SQL, Postgres functions, and even your own metadata to write policies.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tabs */}
<div className="flex flex-col gap-3 items-start justify-end">
{examples.map((example, index) => {
const isActive = index === activeIdx
return (
<button
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex flex-col gap-0.5 py-2 transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<span className="text-2xl font-medium tracking-tight">{example.title}</span>
<span
className={cn(
'text-sm transition-colors',
isActive ? 'text-foreground-lighter' : 'text-foreground-muted'
)}
>
{example.description}
</span>
</button>
)
})}
</div>
{/* Right: code block */}
<motion.div
initial={false}
animate={height != null ? { height } : undefined}
transition={
hasMeasured.current ? { duration: 0.35, ease: [0.22, 1, 0.36, 1] } : { duration: 0 }
}
className="relative self-end overflow-hidden border border-border rounded-md"
>
{/* Invisible measuring copy: drives the animated container height */}
<div
ref={measureRef}
aria-hidden
className="pointer-events-none invisible absolute inset-x-0 top-0 overflow-x-auto pb-12 [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
/>
<AnimatePresence mode="wait">
<motion.div
key={active.title}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="overflow-x-auto pb-12"
>
<div
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
className="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
<div
dangerouslySetInnerHTML={{ __html: active.lightHtml }}
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
<Link
href="/docs/reference/javascript/storage-createbucket"
className="absolute bottom-4 right-4 flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap"
>
Documentation
<svg
width={12}
height={12}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0"
>
<path
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</motion.div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,15 @@
import { QuoteSection as SharedQuoteSection } from '../../_shared/QuoteSection'
export function QuoteSection() {
return (
<SharedQuoteSection
quote="Supabase is great because it has everything. I don't need a different solution for authentication, a different solution for database, or a different solution for storage."
author={{
name: 'Yasser Elsaid',
role: 'Founder of Chatbase',
image: '/images/blog/avatars/yasser-elsaid-chatbase.jpeg',
link: '/customers/chatbase',
}}
/>
)
}
@@ -0,0 +1,39 @@
import { BuiltWithSupabaseSection } from '~/app/(home)/_components/BuiltWithSupabaseSection'
import { CTASection } from '~/app/(home)/_components/CTASection'
import { BucketTypesSection } from './BucketTypesSection'
import { FeaturesSection } from './FeaturesSection'
import { Hero } from './Hero'
import { QuoteSection } from './QuoteSection'
export function StorageContent({
apiSlot,
permissionsSlot,
}: {
apiSlot: React.ReactNode
permissionsSlot: React.ReactNode
}) {
return (
<div className="overflow-x-clip">
<section>
<Hero />
</section>
<section className="border-t border-border">
<FeaturesSection />
</section>
<section className="border-t border-border">
<QuoteSection />
</section>
<section className="border-t border-border">
<BucketTypesSection />
</section>
<section className="border-t border-border">{apiSlot}</section>
<section className="border-t border-border">{permissionsSlot}</section>
<section className="border-t border-border">
<BuiltWithSupabaseSection />
</section>
<section className="border-t border-border">
<CTASection />
</section>
</div>
)
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from 'next'
import { ApiSection } from './_components/ApiSection'
import { PermissionsSection } from './_components/PermissionsSection'
import { StorageContent } from './_components/StorageContent'
export const metadata: Metadata = {
title: 'Storage | Supabase',
description:
'An open source S3 compatible storage solution with unlimited scalability, for any file type.',
}
export default function StoragePage() {
return <StorageContent apiSlot={<ApiSection />} permissionsSlot={<PermissionsSection />} />
}
+76 -87
View File
@@ -1,22 +1,22 @@
'use client'
import { isBrowser, LOCAL_STORAGE_KEYS } from 'common'
import { type BlogView } from 'app/blog/blog-view'
import BlogFilters from 'components/Blog/BlogFilters'
import BlogGridItem from 'components/Blog/BlogGridItem'
import BlogListItem from 'components/Blog/BlogListItem'
import { useInfiniteScrollWithFetch } from 'hooks/useInfiniteScroll'
import { Suspense, useCallback, useState } from 'react'
import type PostTypes from 'types/post'
import { cn } from 'ui'
export type BlogView = 'list' | 'grid'
const POSTS_PER_PAGE = 25
const SKELETON_COUNT = 6
// Featured + 2 secondary posts shown by the layout hero on the index. Excluded
// from the list to avoid duplication — keep in sync with app/blog/layout.tsx.
const HERO_POST_COUNT = 3
function BlogListItemSkeleton() {
return (
<div className="flex flex-col lg:grid lg:grid-cols-10 xl:grid-cols-12 w-full py-2 sm:py-4 h-full border-b">
<div className="flex flex-col lg:grid lg:grid-cols-10 xl:grid-cols-12 w-full py-2 sm:py-4 h-full">
<div className="flex w-full lg:col-span-8 xl:col-span-8">
<div className="h-6 bg-foreground-muted/20 rounded-sm animate-pulse w-3/4" />
</div>
@@ -38,20 +38,16 @@ function BlogListItemSkeleton() {
function BlogGridItemSkeleton() {
return (
<div className="inline-block min-w-full p-2 sm:p-4 h-full">
<div className="flex flex-col space-y-2">
<div className="flex flex-col space-y-1">
<div className="relative mb-3 w-full aspect-2/1 lg:aspect-5/3 overflow-hidden rounded-lg border border-default bg-foreground-muted/20 animate-pulse" />
<div className="flex items-center space-x-1.5">
<div className="h-4 w-24 bg-foreground-muted/20 rounded-sm animate-pulse" />
<div className="h-4 w-4 bg-foreground-muted/20 rounded-sm animate-pulse" />
<div className="h-4 w-16 bg-foreground-muted/20 rounded-sm animate-pulse" />
</div>
<div className="h-6 w-3/4 bg-foreground-muted/20 rounded-sm animate-pulse mt-1" />
<div className="h-4 w-full bg-foreground-muted/20 rounded-sm animate-pulse mt-1" />
<div className="h-4 w-2/3 bg-foreground-muted/20 rounded-sm animate-pulse" />
</div>
<div className="flex flex-col gap-2 p-6">
<div className="relative w-full aspect-[1.91/1] overflow-hidden bg-foreground-muted/20 animate-pulse" />
<div className="flex items-center space-x-1.5 mt-2">
<div className="h-4 w-24 bg-foreground-muted/20 rounded animate-pulse" />
<div className="h-4 w-4 bg-foreground-muted/20 rounded animate-pulse" />
<div className="h-4 w-16 bg-foreground-muted/20 rounded animate-pulse" />
</div>
<div className="h-6 w-3/4 bg-foreground-muted/20 rounded animate-pulse mt-1" />
<div className="h-4 w-full bg-foreground-muted/20 rounded animate-pulse mt-1" />
<div className="h-4 w-2/3 bg-foreground-muted/20 rounded animate-pulse" />
</div>
)
}
@@ -59,12 +55,11 @@ function BlogGridItemSkeleton() {
interface BlogClientProps {
initialBlogs: any[]
totalPosts: number
initialView: BlogView
}
export default function BlogClient({ initialBlogs, totalPosts }: BlogClientProps) {
const { BLOG_VIEW } = LOCAL_STORAGE_KEYS
const localView = isBrowser ? (localStorage?.getItem(BLOG_VIEW) as BlogView) : undefined
const [view, setView] = useState<BlogView>(localView ?? 'list')
export default function BlogClient({ initialBlogs, totalPosts, initialView }: BlogClientProps) {
const [view, setView] = useState<BlogView>(initialView)
const [isFiltering, setIsFiltering] = useState(false)
const [filterParams, setFilterParams] = useState<{ category?: string; search?: string }>({})
const [filteredPosts, setFilteredPosts] = useState<any[] | null>(null)
@@ -76,21 +71,13 @@ export default function BlogClient({ initialBlogs, totalPosts }: BlogClientProps
const fetchMorePosts = useCallback(
async (offset: number, limit: number) => {
const isFiltered =
(filterParams.category && filterParams.category !== 'all') || Boolean(filterParams.search)
// The featured post is rendered above the list (not in `items`), so the
// API offset has to skip past it for unfiltered fetches. Filtered
// results come from a separate query and don't share that hero slot.
const apiOffset = isFiltered ? offset : offset + 1
// The hero posts stay in the loaded array (just rendered in the layout
// hero, sliced off at render), so `offset` already counts them — no skip.
const params = new URLSearchParams({
offset: apiOffset.toString(),
offset: offset.toString(),
limit: limit.toString(),
})
if (filterParams.category && filterParams.category !== 'all') {
params.set('category', filterParams.category)
}
if (filterParams.search) {
params.set('q', filterParams.search)
}
@@ -109,9 +96,7 @@ export default function BlogClient({ initialBlogs, totalPosts }: BlogClientProps
const {
items: blogs,
setItems: setBlogs,
hasMore,
isLoading,
loadMoreRef,
} = useInfiniteScrollWithFetch({
initialItems: currentPosts,
@@ -160,66 +145,70 @@ export default function BlogClient({ initialBlogs, totalPosts }: BlogClientProps
}
}, [])
return (
<>
<Suspense fallback={null}>
<BlogFilters onFilterChange={handleFilterChange} view={view} setView={setView} />
</Suspense>
// The hero posts (the most recent HERO_POST_COUNT) are rendered by the blog
// layout's hero, so drop them from the list to avoid duplication. Filtered
// results are a separate query with no hero, so show all.
const visibleBlogs = filteredPosts !== null ? blogs : blogs.slice(HERO_POST_COUNT)
<ol
className={cn(
'grid -mx-2 sm:-mx-4 py-6 lg:py-6 lg:pb-20',
isList ? 'grid-cols-1' : 'grid-cols-12 lg:gap-4'
)}
>
return (
<div>
<h1 className="sr-only">Supabase blog</h1>
{/* Filters row */}
<div className="sticky top-[65px] z-10 bg-background/80 backdrop-blur-sm border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="py-3">
<Suspense fallback={null}>
<BlogFilters onFilterChange={handleFilterChange} view={view} setView={setView} />
</Suspense>
</div>
</div>
</div>
{/* Blog posts */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)]">
{isFiltering ? (
isList ? (
Array.from({ length: SKELETON_COUNT }).map((_, idx) => (
<div
className="col-span-12 px-2 sm:px-4 [&_a]:last:border-none"
key={`skeleton-list-${idx}`}
>
<BlogListItemSkeleton />
</div>
))
<div>
{Array.from({ length: SKELETON_COUNT }).map((_, idx) => (
<BlogListItemSkeleton key={`skeleton-list-${idx}`} />
))}
</div>
) : (
Array.from({ length: SKELETON_COUNT }).map((_, idx) => (
<div
className="col-span-12 mb-4 md:col-span-12 lg:col-span-6 xl:col-span-4 h-full"
key={`skeleton-grid-${idx}`}
>
<BlogGridItemSkeleton />
</div>
))
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: SKELETON_COUNT }).map((_, idx) => (
<div key={`skeleton-grid-${idx}`}>
<BlogGridItemSkeleton />
</div>
))}
</div>
)
) : blogs?.length ? (
blogs?.map((blog: PostTypes, idx: number) =>
isList ? (
<div
className="col-span-12 px-2 sm:px-4 [&_a]:last:border-none"
key={`list-${idx}-${blog.slug}`}
>
<BlogListItem post={blog} />
</div>
) : (
<div
className="col-span-12 mb-4 md:col-span-12 lg:col-span-6 xl:col-span-4 h-full"
key={`grid-${idx}-${blog.slug}`}
>
<BlogGridItem post={blog} />
</div>
)
) : visibleBlogs?.length ? (
isList ? (
<div>
{visibleBlogs.map((blog: PostTypes, idx: number) => (
<BlogListItem post={blog} key={`list-${idx}-${blog.slug}`} />
))}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{visibleBlogs.map((blog: PostTypes, idx: number) => (
<BlogGridItem post={blog} key={`grid-${idx}-${blog.slug}`} />
))}
</div>
)
) : (
<p className="text-sm text-light col-span-full">No results</p>
<div className="px-6 py-12">
<p className="text-sm text-light">No results</p>
</div>
)}
</ol>
{hasMore && !isFiltering && (
<div ref={loadMoreRef} className="flex justify-center py-8" aria-hidden="true">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-foreground-muted border-t-foreground" />
</div>
)}
</>
{hasMore && !isFiltering && (
<div ref={loadMoreRef} className="flex justify-center py-8" aria-hidden="true">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-foreground-muted border-t-foreground" />
</div>
)}
</div>
</div>
)
}
+104
View File
@@ -0,0 +1,104 @@
'use client'
import FeaturedThumb from 'components/Blog/FeaturedThumb'
import { motion } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import type PostTypes from 'types/post'
import blogAuthors from '@/lib/authors.json'
function SecondarySpotlight({ post }: { post: PostTypes }) {
const resolveImagePath = (img: string | undefined): string | null => {
if (!img) return null
return img.startsWith('/') || img.startsWith('http') ? img : `/images/blog/${img}`
}
const imageUrl =
resolveImagePath(post.imgThumb) ||
resolveImagePath(post.imgSocial) ||
'/images/blog/blog-placeholder.png'
const authorNames = (post.author?.split(',') ?? [])
.map((id) => blogAuthors.find((a: any) => a.author_id === id.trim())?.author)
.filter(Boolean)
.join(', ')
return (
<Link href={post.path} prefetch={false} className="group flex gap-4 items-start">
<div className="relative shrink-0 w-36 aspect-[1.91/1] overflow-hidden rounded-md border border-foreground/10">
<Image
src={imageUrl}
fill
sizes="112px"
quality={80}
className="object-cover group-hover:scale-[1.03] transition-transform duration-300"
alt={post.title}
/>
</div>
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-foreground text-sm leading-snug group-hover:underline line-clamp-3">
{post.title}
</h3>
{post.formattedDate && (
<p className="text-foreground-lighter text-xs">{post.formattedDate}</p>
)}
{authorNames && <p className="text-foreground-light text-xs">{authorNames}</p>}
</div>
</Link>
)
}
/**
* Featured hero for the blog. Rendered by the blog route layout so it stays
* mounted across navigation between `/blog` and `/blog/categories/*`. It's only
* meant for the index, so on other blog routes we animate its height to 0 —
* collapsing it smoothly slides the page content up/down instead of jumping.
*/
export default function BlogHero({
featuredPost,
secondaryPosts,
}: {
featuredPost: PostTypes | null
secondaryPosts: PostTypes[]
}) {
const pathname = usePathname()
// Only mount the hero on the listing routes it animates between (index ⇄
// category). Elsewhere (posts, tags, authors) it's irrelevant — rendering it
// would needlessly preload the featured image.
const isListingRoute = pathname === '/blog' || Boolean(pathname?.startsWith('/blog/categories/'))
const expanded = pathname === '/blog'
if (!featuredPost || !isListingRoute) return null
// When collapsed, `inert` removes the clipped hero (and its links) from the
// a11y tree and tab order entirely.
const collapsedProps = expanded ? {} : ({ inert: '' } as any)
return (
<motion.div
{...collapsedProps}
initial={false}
animate={{ height: expanded ? 'auto' : 0, opacity: expanded ? 1 : 0 }}
transition={{ duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
aria-hidden={!expanded}
className="overflow-hidden"
>
<div className="pt-32 pb-10">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="max-w-4xl">
<FeaturedThumb key={featuredPost.slug} {...featuredPost} />
</div>
{secondaryPosts.length > 0 && (
<div className="mt-14 max-w-4xl grid grid-cols-1 sm:grid-cols-2 gap-6">
{secondaryPosts.map((post) => (
<SecondarySpotlight key={post.slug} post={post} />
))}
</div>
)}
</div>
</div>
</motion.div>
)
}
+31
View File
@@ -0,0 +1,31 @@
'use client'
import Footer from '~/components/Footer'
import Nav from '~/components/Nav'
import { useForceDeepDark } from 'lib/theme.utils'
import type PostTypes from 'types/post'
import BlogHero from './BlogHero'
export default function BlogLayoutShell({
featuredPost,
secondaryPosts,
children,
}: {
featuredPost: PostTypes | null
secondaryPosts: PostTypes[]
children: React.ReactNode
}) {
useForceDeepDark()
return (
<>
<Nav hideNavbar={false} />
<div className="relative w-full [--container-max-w:75rem]">
<BlogHero featuredPost={featuredPost} secondaryPosts={secondaryPosts} />
<main className="relative min-h-screen">{children}</main>
</div>
<Footer />
</>
)
}
@@ -1,17 +1,15 @@
'use client'
import { isBrowser, LOCAL_STORAGE_KEYS } from 'common'
import { type BlogView } from 'app/blog/blog-view'
import BlogGridItem from 'components/Blog/BlogGridItem'
import BlogListItem from 'components/Blog/BlogListItem'
import DefaultLayout from 'components/Layouts/Default'
import { AlignJustify, Grid, Search } from 'lucide-react'
import BlogViewToggle from 'components/Blog/BlogViewToggle'
import { Search } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { useMemo, useState } from 'react'
import type PostTypes from 'types/post'
import { Button, cn, InputGroup, InputGroupAddon, InputGroupInput } from 'ui'
export type BlogView = 'list' | 'grid'
import { InputGroup, InputGroupAddon, InputGroupInput } from 'ui'
interface Author {
author_id: string
@@ -25,12 +23,11 @@ interface AuthorClientProps {
author: Author | null
authorId: string
blogs: PostTypes[]
initialView: BlogView
}
export default function AuthorClient({ author, authorId, blogs }: AuthorClientProps) {
const { BLOG_VIEW } = LOCAL_STORAGE_KEYS
const localView = isBrowser ? (localStorage?.getItem(BLOG_VIEW) as BlogView) : undefined
const [view, setView] = useState<BlogView>(localView ?? 'list')
export default function AuthorClient({ author, authorId, blogs, initialView }: AuthorClientProps) {
const [view, setView] = useState<BlogView>(initialView)
const [searchTerm, setSearchTerm] = useState('')
const isList = view === 'list'
@@ -44,17 +41,10 @@ export default function AuthorClient({ author, authorId, blogs }: AuthorClientPr
)
}, [blogs, searchTerm])
const handleViewSelection = () => {
setView((prevView) => {
const newValue = prevView === 'list' ? 'grid' : 'list'
localStorage.setItem(BLOG_VIEW, newValue)
return newValue
})
}
return (
<DefaultLayout>
<div className="container mx-auto px-4 py-4 md:py-8 xl:py-10 sm:px-16 xl:px-20">
<div>
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-8 md:pt-12 pb-8">
<div className="text-foreground-lighter flex space-x-1 mb-8">
<h1 className="cursor-pointer">
<Link href="/blog">Blog</Link>
@@ -64,7 +54,7 @@ export default function AuthorClient({ author, authorId, blogs }: AuthorClientPr
</div>
{author && (
<div className="flex items-center gap-6 pb-8">
<div className="flex items-center gap-6">
{author.author_image_url && (
<Image
src={author.author_image_url}
@@ -98,9 +88,10 @@ export default function AuthorClient({ author, authorId, blogs }: AuthorClientPr
)}
</div>
<div className="border-default border-t">
<div className="container mx-auto px-4 py-4 md:py-8 xl:py-10 sm:px-16 xl:px-20">
<div className="flex flex-row items-center justify-between gap-2 mb-6">
{/* Filters row — divider above the header, search aligned with the view toggle */}
<div className="sticky top-[65px] z-10 bg-background/80 backdrop-blur-sm border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="py-3 flex flex-row items-center justify-between gap-2">
<div className="flex-1 max-w-[280px]">
<InputGroup className="w-full">
<InputGroupInput
@@ -116,54 +107,37 @@ export default function AuthorClient({ author, authorId, blogs }: AuthorClientPr
</InputGroupAddon>
</InputGroup>
</div>
<Button
type="default"
title={isList ? 'Grid View' : 'List View'}
onClick={handleViewSelection}
className="h-full p-2 text-foreground-light"
>
{isList ? (
<Grid className="w-4 h-4 stroke-1.5" />
) : (
<AlignJustify className="w-4 h-4 stroke-1.5" />
)}
</Button>
<BlogViewToggle view={view} setView={setView} />
</div>
</div>
</div>
{filteredBlogs.length > 0 ? (
<ol
className={cn(
'grid -mx-2 sm:-mx-4 py-6 lg:py-6 lg:pb-20',
isList ? 'grid-cols-1' : 'grid-cols-12 lg:gap-4'
)}
>
{filteredBlogs.map((blog: PostTypes, idx: number) =>
isList ? (
<div
className="col-span-12 px-2 sm:px-4 [&_a]:last:border-none"
key={`list-${idx}-${blog.slug}`}
>
<BlogListItem post={blog} />
</div>
) : (
<div
className="col-span-12 mb-4 md:col-span-12 lg:col-span-6 xl:col-span-4 h-full"
key={`grid-${idx}-${blog.slug}`}
>
<BlogGridItem post={blog} />
</div>
)
)}
</ol>
{/* Posts */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)]">
{filteredBlogs.length > 0 ? (
isList ? (
<div>
{filteredBlogs.map((blog, idx) => (
<BlogListItem post={blog} key={`list-${idx}-${blog.slug}`} />
))}
</div>
) : (
<p className="text-foreground-lighter py-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{filteredBlogs.map((blog, idx) => (
<BlogGridItem post={blog} key={`grid-${idx}-${blog.slug}`} />
))}
</div>
)
) : (
<div className="px-6 py-12">
<p className="text-sm text-light">
{searchTerm
? 'No posts found matching your search.'
: 'No posts found by this author.'}
</p>
)}
</div>
</div>
)}
</div>
</DefaultLayout>
</div>
)
}
+21 -8
View File
@@ -1,4 +1,7 @@
import { BLOG_VIEW_COOKIE, isBlogView, type BlogView } from 'app/blog/blog-view'
import type { Metadata } from 'next'
import { cookies } from 'next/headers'
import { notFound } from 'next/navigation'
import AuthorClient from './AuthorClient'
import blogAuthors from '@/lib/authors.json'
@@ -21,13 +24,6 @@ function toCanonicalAuthorId(identifier: string): string {
return authorIdLookup.get(identifier) ?? identifier
}
export async function generateStaticParams() {
return blogAuthors.map((author) => ({ author: author.author_id }))
}
export const revalidate = 30
export const dynamic = 'force-static'
export async function generateMetadata({
params: paramsPromise,
}: {
@@ -47,6 +43,15 @@ export default async function AuthorPage({ params: paramsPromise }: { params: Pr
const authorId = params.author
const author = blogAuthors.find((a) => a.author_id === authorId) ?? null
if (!author) {
notFound()
}
// Read the list/grid preference from a cookie so the correct view renders on
// first paint. Reading a cookie opts this route into dynamic rendering.
const cookieStore = await cookies()
const cookieView = cookieStore.get(BLOG_VIEW_COOKIE)?.value
const initialView: BlogView = isBlogView(cookieView) ? cookieView : 'list'
// Get static posts where author field contains this author_id (normalize identifiers)
const staticPosts = getSortedPosts({ directory: '_blog', limit: 0 }).filter((post: any) => {
@@ -58,5 +63,13 @@ export default async function AuthorPage({ params: paramsPromise }: { params: Pr
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
) as unknown as PostTypes[]
return <AuthorClient author={author} authorId={authorId} blogs={blogs} />
return (
<AuthorClient
key={authorId}
author={author}
authorId={authorId}
blogs={blogs}
initialView={initialView}
/>
)
}
+17
View File
@@ -0,0 +1,17 @@
export type BlogView = 'list' | 'grid'
export const BLOG_VIEW_COOKIE = 'blog-view'
export function isBlogView(value: string | undefined | null): value is BlogView {
return value === 'list' || value === 'grid'
}
/**
* Persist the list/grid preference in a cookie so the server can render the
* correct view on the next load. Storing it client-side (localStorage) caused a
* hydration flicker: the server rendered the default `list`, then the client
* swapped to `grid`. Client-side only — call from an event handler.
*/
export function setBlogViewCookie(view: BlogView) {
document.cookie = `${BLOG_VIEW_COOKIE}=${view}; path=/; max-age=31536000; samesite=lax`
}
@@ -0,0 +1,69 @@
'use client'
import { type BlogView } from 'app/blog/blog-view'
import BlogFilters from 'components/Blog/BlogFilters'
import BlogGridItem from 'components/Blog/BlogGridItem'
import BlogListItem from 'components/Blog/BlogListItem'
import { Suspense, useMemo, useState } from 'react'
import type PostTypes from 'types/post'
export default function CategoryClient({
posts,
initialView,
}: {
posts: PostTypes[]
initialView: BlogView
}) {
const [view, setView] = useState<BlogView>(initialView)
const [searchTerm, setSearchTerm] = useState('')
const isList = view === 'list'
// In-category search: filter the (already loaded) posts client-side.
const visiblePosts = useMemo(() => {
if (!searchTerm) return posts
const term = searchTerm.toLowerCase()
return posts.filter(
(post) =>
(post.title ?? '').toLowerCase().includes(term) ||
(post.description ?? '').toLowerCase().includes(term)
)
}, [posts, searchTerm])
return (
<div>
{/* Filters row — categories navigate; search filters within this category */}
<div className="sticky top-[65px] z-10 bg-background/80 backdrop-blur-sm border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="py-3">
<Suspense fallback={null}>
<BlogFilters view={view} setView={setView} onSearch={setSearchTerm} />
</Suspense>
</div>
</div>
</div>
{/* Posts */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)]">
{visiblePosts.length ? (
isList ? (
<div>
{visiblePosts.map((post, idx) => (
<BlogListItem post={post} key={`list-${idx}-${post.slug}`} />
))}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{visiblePosts.map((post, idx) => (
<BlogGridItem post={post} key={`grid-${idx}-${post.slug}`} />
))}
</div>
)
) : (
<div className="px-6 py-12">
<p className="text-sm text-light">No results</p>
</div>
)}
</div>
</div>
)
}
@@ -1,22 +1,15 @@
import { BLOG_VIEW_COOKIE, isBlogView, type BlogView } from 'app/blog/blog-view'
import type { Metadata } from 'next'
import Link from 'next/link'
import { cookies } from 'next/headers'
import { notFound } from 'next/navigation'
import BlogGridItem from '@/components/Blog/BlogGridItem'
import DefaultLayout from '@/components/Layouts/Default'
import CategoryClient from './CategoryClient'
import { capitalize } from '@/lib/helpers'
import { getAllCategories, getSortedPosts } from '@/lib/posts'
import type PostTypes from '@/types/post'
type Params = { category: string }
export async function generateStaticParams() {
const categories = getAllCategories('_blog')
return categories.map((category: string) => ({ category }))
}
export const revalidate = 30
export const dynamic = 'force-static'
export async function generateMetadata({
params: paramsPromise,
}: {
@@ -38,37 +31,24 @@ export default async function CategoriesPage({
}) {
const params = await paramsPromise
if (!getAllCategories('_blog').includes(params.category)) {
notFound()
}
// Read the list/grid preference from a cookie so the correct view renders on
// first paint. Reading a cookie opts this route into dynamic rendering.
const cookieStore = await cookies()
const cookieView = cookieStore.get(BLOG_VIEW_COOKIE)?.value
const initialView: BlogView = isBlogView(cookieView) ? cookieView : 'list'
const staticPosts = getSortedPosts({
directory: '_blog',
limit: 0,
categories: [params.category],
})
const blogs = [...staticPosts] as PostTypes[]
const capitalizedCategory = capitalize(params?.category.replaceAll('-', ' '))
return (
<>
<DefaultLayout>
<div className="container mx-auto px-8 py-16 sm:px-16 xl:px-20">
<div className="text-foreground-lighter flex space-x-1">
<h1 className="cursor-pointer">
<Link href="/blog">Blog</Link>
<span className="px-2">/</span>
<span>{capitalizedCategory}</span>
</h1>
</div>
<ol className="grid grid-cols-12 gap-8 py-16 lg:gap-16">
{blogs.map((blog: PostTypes) => (
<div
className="col-span-12 mb-16 md:col-span-12 lg:col-span-6 xl:col-span-4"
key={blog.slug}
>
<BlogGridItem post={blog} />
</div>
))}
</ol>
</div>
</DefaultLayout>
</>
)
// Key by category so state (search term, view) resets when switching between
// category pages rather than persisting across the reused page component.
return <CategoryClient key={params.category} posts={blogs} initialView={initialView} />
}
+23
View File
@@ -0,0 +1,23 @@
import BlogLayoutShell from './BlogLayoutShell'
import { getSortedPosts } from '@/lib/posts'
import type PostTypes from '@/types/post'
export default async function BlogLayout({ children }: { children: React.ReactNode }) {
// Fetched once in the (persistent) layout so the featured hero survives
// navigation between blog routes and can animate its height on change.
const staticPosts = getSortedPosts({ directory: '_blog' })
const sorted = [...staticPosts].sort((a, b) => {
const aDate = new Date(a.date || a.formattedDate).getTime()
const bDate = new Date(b.date || b.formattedDate).getTime()
return bDate - aDate
})
const featuredPost = (sorted[0] ?? null) as PostTypes | null
const secondaryPosts = sorted.slice(1, 3) as PostTypes[]
return (
<BlogLayoutShell featuredPost={featuredPost} secondaryPosts={secondaryPosts}>
{children}
</BlogLayoutShell>
)
}
+16 -25
View File
@@ -1,15 +1,12 @@
import FeaturedThumb from 'components/Blog/FeaturedThumb'
import DefaultLayout from 'components/Layouts/Default'
import type { Metadata } from 'next'
import type PostTypes from 'types/post'
import { cookies } from 'next/headers'
import { BLOG_VIEW_COOKIE, isBlogView, type BlogView } from './blog-view'
import BlogClient from './BlogClient'
import { breadcrumbs } from '@/lib/breadcrumbs'
import { breadcrumbListSchema, serializeJsonLd } from '@/lib/json-ld'
import { getSortedPosts } from '@/lib/posts'
export const revalidate = 30
export const metadata: Metadata = {
title: 'Supabase Blog: the Postgres development platform',
description: 'Get all your Supabase News on the Supabase blog.',
@@ -32,13 +29,16 @@ export default async function BlogPage() {
return dateB - dateA
})
// The featured post + secondary spotlights are derived from the head of this
// list inside BlogClient; chrome (nav, footer, container) comes from the blog
// route's layout.tsx.
const initialPosts = allPosts.slice(0, INITIAL_POSTS_LIMIT)
const featuredPost = initialPosts[0]
// Featured post is rendered as the hero above the list, so exclude it from
// the list to avoid showing it twice. BlogClient compensates the API offset
// when scrolling for more posts.
const listPosts = initialPosts.slice(1)
const totalListPosts = Math.max(0, allPosts.length - 1)
// Render the list/grid view from a cookie so it matches the user's choice on
// first paint (avoids the localStorage hydration flicker).
const cookieStore = await cookies()
const cookieView = cookieStore.get(BLOG_VIEW_COOKIE)?.value
const initialView: BlogView = isBlogView(cookieView) ? cookieView : 'list'
return (
<>
@@ -48,20 +48,11 @@ export default async function BlogPage() {
__html: serializeJsonLd(breadcrumbListSchema(breadcrumbs.blogIndex)),
}}
/>
<DefaultLayout>
<h1 className="sr-only">Supabase blog</h1>
<div className="container relative mx-auto px-4 py-4 md:py-8 xl:py-10 sm:px-16 xl:px-20">
{featuredPost && (
<FeaturedThumb key={featuredPost.slug} {...(featuredPost as PostTypes)} />
)}
</div>
<div className="border-default border-t">
<div className="container mx-auto px-4 py-4 md:py-8 xl:py-10 sm:px-16 xl:px-20">
<BlogClient initialBlogs={listPosts} totalPosts={totalListPosts} />
</div>
</div>
</DefaultLayout>
<BlogClient
initialBlogs={initialPosts}
totalPosts={allPosts.length}
initialView={initialView}
/>
</>
)
}
+9 -2
View File
@@ -4,11 +4,13 @@ import 'config/code-hike.css'
import '../styles/globals.css'
import '../pages/launch-week/launchWeek.css'
import { APP_NAME, DEFAULT_META_DESCRIPTION } from '~/lib/constants'
import { geistMono, inter, manrope } from '~/lib/fonts'
import { Agentation } from 'agentation'
import { Metadata } from 'next'
import type { Viewport } from 'next'
import Providers from './providers'
import { APP_NAME, DEFAULT_META_DESCRIPTION } from '@/lib/constants'
const site_title = `${APP_NAME} | The Open Source Firebase Alternative`
@@ -47,9 +49,14 @@ export const viewport: Viewport = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<html
lang="en"
suppressHydrationWarning
className={`${manrope.variable} ${inter.variable} ${geistMono.variable}`}
>
<body>
<Providers>{children}</Providers>
{process.env.NODE_ENV === 'development' && <Agentation endpoint="http://localhost:4747" />}
</body>
</html>
)
+31 -23
View File
@@ -12,7 +12,9 @@ import {
import { WwwCommandMenu } from 'components/CommandMenu'
import { DevToolbar, DevToolbarProvider } from 'dev-tools'
import { API_URL } from 'lib/constants'
import { TooltipProvider } from 'ui'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { Suspense } from 'react'
import { themes, TooltipProvider } from 'ui'
import { CommandProvider } from 'ui-patterns/CommandMenu'
import { useConsentToast } from 'ui-patterns/consent'
@@ -23,28 +25,34 @@ function Providers({ children }: { children: React.ReactNode }) {
const { hasAcceptedConsent } = useConsentToast()
return (
<AuthProvider>
<FeatureFlagProvider API_URL={API_URL} enabled={IS_PLATFORM}>
<DevToolbarProvider apiUrl={API_URL}>
<ThemeProvider>
<TooltipProvider delayDuration={0}>
<CommandProvider>
<TelemetryTagManager />
<Toaster />
{children}
<WwwCommandMenu />
<PageTelemetry
API_URL={API_URL}
hasAcceptedConsent={hasAcceptedConsent}
enabled={IS_PLATFORM}
/>
<DevToolbar />
</CommandProvider>
</TooltipProvider>
</ThemeProvider>
</DevToolbarProvider>
</FeatureFlagProvider>
</AuthProvider>
<NuqsAdapter>
<AuthProvider>
<FeatureFlagProvider API_URL={API_URL} enabled={IS_PLATFORM}>
<DevToolbarProvider apiUrl={API_URL}>
<ThemeProvider
themes={themes.map((t) => t.value)}
enableSystem
disableTransitionOnChange
>
<TooltipProvider delayDuration={0}>
<CommandProvider>
<TelemetryTagManager />
<Toaster />
<Suspense fallback={null}>{children}</Suspense>
<WwwCommandMenu />
<PageTelemetry
API_URL={API_URL}
hasAcceptedConsent={hasAcceptedConsent}
enabled={IS_PLATFORM}
/>
<DevToolbar />
</CommandProvider>
</TooltipProvider>
</ThemeProvider>
</DevToolbarProvider>
</FeatureFlagProvider>
</AuthProvider>
</NuqsAdapter>
)
}
@@ -0,0 +1,46 @@
import Image from 'next/image'
import type Author from '@/types/author'
interface Props {
authors: (Author | undefined)[]
showName?: boolean
size?: 'sm' | 'md'
}
export default function AuthorAvatars({ authors, showName = true, size = 'sm' }: Props) {
const valid = authors.filter(Boolean) as Author[]
if (!valid.length) return null
const px = size === 'md' ? 'w-6 h-6' : 'w-5 h-5'
return (
<div className="flex items-center gap-2">
<div className="flex items-center -space-x-1.5">
{valid.map((author, i) => (
<div
key={i}
className={`relative ${px} rounded-full ring-2 ring-background border border-foreground/20 overflow-hidden shrink-0`}
>
{author.author_image_url && (
<Image
src={
typeof author.author_image_url === 'string'
? author.author_image_url
: (author.author_image_url as { url: string }).url
}
fill
className="object-cover"
alt={author.author}
/>
)}
</div>
))}
</div>
{showName && (
<p className="text-foreground-lighter text-xs truncate">
{valid.map((a) => a.author).join(', ')}
</p>
)}
</div>
)
}
+96 -129
View File
@@ -1,10 +1,13 @@
'use client'
import type { BlogView } from 'app/blog/BlogClient'
import { LOCAL_STORAGE_KEYS, useBreakpoint } from 'common'
import { type BlogView } from 'app/blog/blog-view'
import { useBreakpoint } from 'common'
import BlogViewToggle from 'components/Blog/BlogViewToggle'
import { motion } from 'framer-motion'
import { startCase } from 'lib/helpers'
import { AlignJustify, ChevronDown, X as CloseIcon, Grid, Search } from 'lucide-react'
import { useRouter, useSearchParams } from 'next/navigation'
import { ChevronDown, X as CloseIcon, Search } from 'lucide-react'
import Link from 'next/link'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react'
import {
Button,
@@ -20,68 +23,73 @@ import {
} from 'ui'
interface Props {
onFilterChange: (category?: string, search?: string) => void
view: BlogView
setView: (view: any) => void
/**
* Blog index search: keeps the query in the URL (`?q=`) and filters the full
* post set client-side via the index's fetch logic.
*/
onFilterChange?: (category?: string, search?: string) => void
/**
* Category-page search: filters the in-memory posts for the current category.
* No URL sync / fetch. Mutually exclusive with `onFilterChange`.
*/
onSearch?: (term: string) => void
}
/**
* ✅ search via text input
* ✅ update searchTerm when deleting text input
* ✅ search via q param
* ✅ search via category if no q param
* ✅ search via category and reset q param if present
*/
// Hard-coded so they can be curated/reordered. Each (except "all") maps to a
// statically generated /blog/categories/<category> page, so selecting a
// category is a navigation (prefetched, no client fetch / skeleton flash).
const allCategories = [
'all',
'product',
'company',
'postgres',
'developers',
'engineering',
'launch-week',
]
function BlogFilters({ onFilterChange, view, setView }: Props) {
const { BLOG_VIEW } = LOCAL_STORAGE_KEYS
const isList = view === 'list'
const [category, setCategory] = useState<string>('all')
const categoryHref = (category: string) =>
category === 'all' ? '/blog' : `/blog/categories/${category}`
const categoryLabel = (category: string) =>
category === 'all' ? 'All' : startCase(category.replaceAll('-', ' '))
function BlogFilters({ view, setView, onFilterChange, onSearch }: Props) {
const showSearch = Boolean(onFilterChange) || Boolean(onSearch)
const [searchTerm, setSearchTerm] = useState<string>('')
const [showSearchInput, setShowSearchInput] = useState<boolean>(false)
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const q = searchParams?.get('q')
const activeCategory = searchParams?.get('category')
const isMobile = useBreakpoint(1023)
const is2XL = useBreakpoint(1535)
// Use hard-coded categories here as they:
// - serve as a reference
// - are easier to reorder
const allCategories = [
'all',
'product',
'company',
'postgres',
'developers',
'engineering',
'launch-week',
]
// Active category is derived from the URL: /blog or /blog/categories/<cat>.
const categoryMatch = pathname?.match(/^\/blog\/categories\/([^/]+)/)
const activeCategory = categoryMatch?.[1] ?? 'all'
// Debounced filter change to avoid too many API calls
// Debounced search to avoid too many API calls
const debouncedFilterChange = useCallback(
(() => {
let timeoutId: NodeJS.Timeout
return (cat: string, search: string) => {
return (search: string) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
onFilterChange(cat, search)
onFilterChange?.(undefined, search)
}, 300)
}
})(),
[onFilterChange]
)
// Handle URL params on mount
// Reflect ?q= deep links into the search state on mount (index only)
useEffect(() => {
if (q) {
if (onFilterChange && q) {
setSearchTerm(q)
onFilterChange(category, q)
} else if (activeCategory && activeCategory !== 'all') {
setCategory(activeCategory)
onFilterChange(activeCategory, '')
onFilterChange(undefined, q)
}
}, []) // Only run on mount
@@ -89,17 +97,18 @@ function BlogFilters({ onFilterChange, view, setView }: Props) {
(text: string) => {
setSearchTerm(text)
// Update URL
if (text.length > 0) {
router?.replace(`/blog?q=${text}`, { scroll: false })
if (onFilterChange) {
// Index: keep the query in the URL and filter the full list (debounced).
router?.replace(text.length > 0 ? `/blog?q=${encodeURIComponent(text)}` : '/blog', {
scroll: false,
})
debouncedFilterChange(text)
} else {
router?.replace('/blog', { scroll: false })
// Category: filter the in-memory posts; no URL change.
onSearch?.(text)
}
// Trigger filter change (debounced)
debouncedFilterChange(category, text)
},
[category, router, debouncedFilterChange]
[router, debouncedFilterChange, onFilterChange, onSearch]
)
useEffect(() => {
@@ -116,43 +125,9 @@ function BlogFilters({ onFilterChange, view, setView }: Props) {
setShowSearchInput(!isMobile)
}, [isMobile])
const handleSetCategory = useCallback(
(newCategory: string) => {
setSearchTerm('')
setCategory(newCategory)
// Update URL
if (newCategory === 'all') {
router?.replace('/blog', { scroll: false })
} else {
router?.replace(`/blog?category=${newCategory}`, { scroll: false })
}
// Trigger filter change immediately for category changes
onFilterChange(newCategory, '')
},
[router, onFilterChange]
)
const handleSearchChange = useCallback(
(event: any) => {
setCategory('all')
handleSearchByText(event.target.value)
},
[handleSearchByText]
)
const handleViewSelection = () => {
setView((prevView: 'list' | 'grid') => {
const newValue = prevView === 'list' ? 'grid' : 'list'
localStorage.setItem(BLOG_VIEW, newValue)
return newValue
})
}
return (
<div className="flex flex-row items-center justify-between gap-2">
{/* Mobile: category dropdown (hidden on lg+) */}
{!showSearchInput && (
<div className="flex lg:hidden">
<DropdownMenu>
@@ -162,53 +137,54 @@ function BlogFilters({ onFilterChange, view, setView }: Props) {
iconRight={<ChevronDown />}
className="w-full min-w-[200px] flex justify-between items-center py-2"
>
{!activeCategory ? 'All Posts' : startCase(activeCategory?.replaceAll('-', ' '))}
{activeCategory === 'all' ? 'All Posts' : categoryLabel(activeCategory)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start">
{allCategories.map((category: string, i: number) => (
{allCategories.map((category) => (
<DropdownMenuItem
key={`item-${category}-${
// biome-ignore lint/suspicious/noArrayIndexKey: to disambiguate emtpy values
i
}`}
onClick={() => handleSetCategory(category)}
className={cn(
(category === 'all' && !activeCategory) || category === activeCategory
? 'text-brand-600'
: ''
)}
key={category}
onClick={() => router.push(categoryHref(category))}
className={cn(category === activeCategory ? 'text-brand-600' : '')}
>
{category === 'all' ? 'All Posts' : startCase(category.replaceAll('-', ' '))}
{category === 'all' ? 'All Posts' : categoryLabel(category)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
<div className="hidden lg:flex flex-wrap items-center grow gap-2">
{allCategories.map((category: string) => (
<Button
key={category}
type={
category === 'all' && !searchTerm && !activeCategory
? 'default'
: category === activeCategory
? 'default'
: 'outline'
}
onClick={() => handleSetCategory(category)}
size={is2XL ? 'tiny' : 'small'}
className="rounded-full"
>
{category === 'all' ? 'All' : startCase(category.replaceAll('-', ' '))}
</Button>
))}
{/* Desktop: category pills */}
<div className="hidden lg:flex flex-wrap items-center flex-grow gap-1">
{allCategories.map((category) => {
const isActive = category === activeCategory
return (
<Link
key={category}
href={categoryHref(category)}
scroll={false}
className={cn(
'relative px-3 py-1.5 text-sm rounded-full transition-colors',
isActive ? 'text-foreground' : 'text-foreground-lighter hover:text-foreground-light'
)}
>
{isActive && (
<motion.span
layoutId="blog-cat-bg"
className="absolute inset-0 rounded-full bg-surface-300 border border-border"
transition={{ type: 'spring', duration: 0.35, bounce: 0.15 }}
/>
)}
<span className="relative z-10">{categoryLabel(category)}</span>
</Link>
)
})}
</div>
{!showSearchInput && (
{/* Search (index only) */}
{showSearch && !showSearchInput && (
<div className="flex-1 flex justify-end">
{' '}
<Button
className="px-2 h-full"
size="medium"
@@ -220,7 +196,7 @@ function BlogFilters({ onFilterChange, view, setView }: Props) {
</div>
)}
{showSearchInput && (
{showSearch && showSearchInput && (
<div className="w-full h-auto flex justify-end gap-2 items-stretch lg:max-w-[240px] xl:max-w-[280px]">
<InputGroup className="w-full">
<InputGroupInput
@@ -229,7 +205,7 @@ function BlogFilters({ onFilterChange, view, setView }: Props) {
type="search"
placeholder="Search blog"
value={searchTerm}
onChange={handleSearchChange}
onChange={(event) => handleSearchByText(event.target.value)}
/>
<InputGroupAddon>
<Search />
@@ -249,18 +225,9 @@ function BlogFilters({ onFilterChange, view, setView }: Props) {
</InputGroup>
</div>
)}
<Button
type="default"
title={isList ? 'Grid View' : 'List View'}
onClick={handleViewSelection}
className="h-full p-2 text-foreground-light"
>
{isList ? (
<Grid className="w-4 h-4 stroke-1.5" />
) : (
<AlignJustify className="w-4 h-4 stroke-1.5" />
)}
</Button>
{/* View toggle */}
<BlogViewToggle view={view} setView={setView} />
</div>
)
}
+28 -34
View File
@@ -2,12 +2,9 @@ import dayjs from 'dayjs'
import Image from 'next/image'
import Link from 'next/link'
import AuthorAvatars from './AuthorAvatars'
import authors from '@/lib/authors.json'
import {
BLOG_GRID_IMAGE_SIZES,
BLOG_PLACEHOLDER_IMAGE,
getBlogThumbnailImage,
} from '@/lib/blog-images'
import { BLOG_PLACEHOLDER_IMAGE, getBlogThumbnailImage } from '@/lib/blog-images'
import type Author from '@/types/author'
import type PostTypes from '@/types/post'
@@ -32,36 +29,33 @@ const BlogGridItem = ({ post }: Props) => {
const imageUrl = getBlogThumbnailImage(post) ?? BLOG_PLACEHOLDER_IMAGE
return (
<Link
href={post.path}
prefetch={false}
className="group inline-block min-w-full p-2 sm:p-4 h-full border border-transparent transition-all hover:bg-surface-200 dark:hover:bg-surface-75 rounded-xl"
>
<div className="flex flex-col space-y-2">
<div className="flex flex-col space-y-1">
<div className="border-default relative mb-3 w-full aspect-[1.91/1] overflow-hidden rounded-lg border shadow-xs">
<Image
fill
sizes={BLOG_GRID_IMAGE_SIZES}
src={imageUrl}
className="scale-100 object-cover overflow-hidden"
alt={`${post.title} thumbnail`}
/>
<Link href={post.path} prefetch={false} className="group flex flex-col h-full p-6">
<div className="relative w-full aspect-[1.91/1] overflow-hidden rounded-md border border-foreground/10">
<Image
fill
sizes="100%"
quality={100}
src={imageUrl}
className="object-cover"
alt={`${post.title} thumbnail`}
/>
</div>
<div className="flex flex-col gap-1 pt-4">
<h3 className="text-foreground text-lg group-hover:underline">{post.title}</h3>
<p className="text-foreground-lighter text-sm mt-1 line-clamp-2">{post.description}</p>
{post.date && (
<div className="text-foreground-lighter flex items-center space-x-1.5 text-[11px] mt-3">
<p>{dayjs(post.date).format('D MMM YYYY')}</p>
{post.readingTime && (
<>
<p>•</p>
<p>{post.readingTime}</p>
</>
)}
</div>
{post.date && (
<div className="text-foreground-lighter flex items-center space-x-1.5 text-sm">
<p>{dayjs(post.date).format('D MMM YYYY')}</p>
{post.readingTime && (
<>
<p>·</p>
<p>{post.readingTime}</p>
</>
)}
</div>
)}
<h3 className="text-foreground max-w-sm text-xl">{post.title}</h3>
<p className="text-foreground-light max-w-sm text-base mb-0!">{post.description}</p>
)}
<div className="mt-1.5">
<AuthorAvatars authors={author} />
</div>
</div>
</Link>
+5 -19
View File
@@ -1,11 +1,11 @@
import dayjs from 'dayjs'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import { Badge } from 'ui'
import blogAuthors from '@/lib/authors.json'
import type PostTypes from '@/types/post'
import AuthorAvatars from './AuthorAvatars'
interface Props {
post: PostTypes
@@ -35,28 +35,14 @@ const BlogListItem = ({ post }: Props) => {
<Link
href={post.path}
prefetch={false}
className="group flex flex-col lg:grid lg:grid-cols-10 xl:grid-cols-12 w-full py-2 sm:py-4 h-full border-b"
className="group flex flex-col lg:grid lg:grid-cols-10 xl:grid-cols-12 w-full py-2 sm:py-4 h-full hover:bg-surface-75/50 transition-colors px-6"
>
<div className="flex w-full lg:col-span-8 xl:col-span-8">
<h3 className="text-foreground text-lg group-hover:underline">{post.title}</h3>
<h3 className="text-foreground text-sm group-hover:underline">{post.title}</h3>
</div>
<div className="lg:col-span-2 xl:col-span-4 flex justify-start items-center lg:grid grid-cols-2 xl:grid-cols-3 gap-2 text-sm">
<div className="hidden lg:flex items-center -space-x-2">
{authors.map((author: any, i: number) => {
if (!author) return null
return (
<div className="relative ring-background w-6 h-6 rounded-full ring-2" key={i}>
{author.author_image_url && (
<Image
src={author.author_image_url}
className="rounded-full object-cover border border-default w-full h-full"
alt={`${author.author} avatar`}
fill
/>
)}
</div>
)
})}
<div className="hidden lg:block">
<AuthorAvatars authors={authors} showName={false} size="md" />
</div>
{post.categories && (
<div className="hidden xl:flex text-foreground-lighter group-hover:text-foreground-light">
+164 -165
View File
@@ -1,14 +1,14 @@
import dayjs from 'dayjs'
import { ChevronLeft } from 'lucide-react'
import { ArrowLeft } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import type { PostReturnType, ProcessedBlogData, StaticAuthor, Tag } from 'types/post'
import { Badge } from 'ui'
import { CTASection } from '@/app/(home)/_components/CTASection'
import DraftModeBanner from '@/components/Blog/DraftModeBanner'
import ShareArticleActions from '@/components/Blog/ShareArticleActions'
import CTABanner from '@/components/CTABanner'
import BlogLinks from '@/components/LaunchWeek/7/BlogLinks'
import LW11Summary from '@/components/LaunchWeek/11/LW11Summary'
import LW12Summary from '@/components/LaunchWeek/12/LWSummary'
@@ -16,8 +16,7 @@ import LW13Summary from '@/components/LaunchWeek/13/Releases/LWSummary'
import LW14Summary from '@/components/LaunchWeek/14/Releases/LWSummary'
import LW15Summary from '@/components/LaunchWeek/15/LWSummary'
import LWXSummary from '@/components/LaunchWeek/X/LWXSummary'
import DefaultLayout from '@/components/Layouts/Default'
import { BLOG_POST_HERO_IMAGE_SIZES, getBlogThumbnailImage } from '@/lib/blog-images'
import { getBlogThumbnailImage } from '@/lib/blog-images'
import { compileBlogMdx } from '@/lib/mdx/compileBlogMdx'
import mdxComponents from '@/lib/mdx/mdxComponents'
@@ -82,7 +81,7 @@ const BlogPostRenderer = async ({
const toc = blogMetaData.toc && (
<div>
<p className="text-foreground mb-4">On this page</p>
<p className="text-foreground-lighter text-sm font-normal mb-3">On this page</p>
<div className="prose-toc">
{blogMetaData.toc && (
<ReactMarkdown>
@@ -102,185 +101,185 @@ const BlogPostRenderer = async ({
return (
<>
{isDraftMode && <DraftModeBanner />}
<DefaultLayout className="overflow-x-hidden">
<div
className="
container mx-auto px-4 py-4 md:py-8 xl:py-10 sm:px-16
xl:px-20
"
>
<div className="grid grid-cols-12 gap-4">
<div className="hidden col-span-12 xl:block lg:col-span-2">
{/* Back button */}
<div className="overflow-x-clip">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
<div className="grid grid-cols-12 xl:gap-x-8 xl:gap-y-0">
{/* Row 1, Col 1: Back button */}
<div className="col-span-12 xl:row-start-1 xl:col-start-1 xl:col-span-1 pt-16 xl:pt-32 flex items-start justify-start">
<Link
href="/blog"
className="text-foreground-lighter hover:text-foreground flex cursor-pointer items-center text-sm transition"
className="text-foreground-lighter hover:text-foreground inline-flex cursor-pointer items-center text-sm transition"
>
<ChevronLeft style={{ padding: 0 }} />
Back
<ArrowLeft strokeWidth={1.5} className="size-4" />
Blog
</Link>
</div>
<div className="col-span-12 lg:col-span-12 xl:col-span-10">
{/* Title and description */}
<div className="mb-6 lg:mb-10 max-w-5xl space-y-8">
<div className="space-y-4">
<Link href="/blog" className="text-brand hidden lg:inline-flex items-center">
Blog
</Link>
<h1 className="text-2xl sm:text-4xl">{blogMetaData.title}</h1>
<div className="text-light flex space-x-3 text-sm">
{/* Row 1, Col 2: Title + meta */}
<div className="col-span-12 xl:row-start-1 xl:col-start-2 xl:col-span-7 pt-4 xl:pt-32 pb-6 space-y-4">
<div className="space-y-4">
{(blogMetaData.tags as Tag[])?.length > 0 && (
<div className="flex flex-wrap gap-2">
{(blogMetaData.tags as Tag[])?.map((tag) => {
const tagName = typeof tag === 'string' ? tag : tag.name
const tagId = typeof tag === 'string' ? tag : tag.id.toString()
return (
<Link
className="flex"
href={`/blog/tags/${tagName}`}
key={`category-badge-${tagId}`}
>
<Badge>{tagName}</Badge>
</Link>
)
})}
</div>
)}
<h1 className="text-2xl sm:text-4xl max-w-3xl">{blogMetaData.title}</h1>
<div className="flex items-center gap-2">
<div className="text-foreground-lighter flex space-x-3 text-sm">
<p>{dayjs(blogMetaData.date).format('DD MMM YYYY')}</p>
<p>·</p>
<p>{(blogMetaData as any).readingTime}</p>
</div>
{authors.length > 0 && (
<div className="flex justify-between">
<div className="flex-1 flex flex-wrap gap-3 pt-2 md:gap-0 lg:gap-3">
{authors.map((author, i: number) => {
const authorImageUrl = author.author_image_url
const authorId =
(author as any).author_id ||
(author as any).username ||
author.author.toLowerCase().replace(/\s+/g, '_')
return (
<div className="mr-4 w-max" key={`author-${i}-${author.author}`}>
<Link href={`/blog/authors/${authorId}`} className="cursor-pointer">
<div className="flex items-center gap-3">
{authorImageUrl && (
<div className="w-10">
<Image
src={authorImageUrl}
className="border-default rounded-full border w-full max-h-10 aspect-square object-cover"
alt={`${author.author} avatar`}
width={40}
height={40}
/>
</div>
)}
<div className="flex flex-col">
<span className="text-foreground mb-0 text-sm">
{author.author}
</span>
<span className="text-foreground-lighter mb-0 text-xs">
{author.position}
</span>
</div>
</div>
</Link>
</div>
{authors.length > 0 && (
<div className="flex items-center gap-4 mt-6">
{authors.map((author, i: number) => {
const authorImageUrl = author.author_image_url
const authorId =
(author as any).author_id ||
(author as any).username ||
author.author.toLowerCase().replace(/\s+/g, '_')
return (
<Link
href={`/blog/authors/${authorId}`}
className="cursor-pointer"
key={`author-${i}-${author.author}`}
>
<div className="flex items-center gap-2">
{authorImageUrl && (
<Image
src={authorImageUrl}
className="border-default rounded-full border aspect-square object-cover"
alt={`${author.author} avatar`}
width={32}
height={32}
/>
)}
<div className="flex flex-col">
<span className="text-foreground text-sm">{author.author}</span>
<span className="text-foreground-lighter text-xs">
{author.position}
</span>
</div>
)
})}
</div>
</div>
</div>
</Link>
)
})}
</div>
)}
</div>
</div>
{/* Row 2: Hero image — col-start-1 col-span-9 = equal overhang each side of content (col 2–8) */}
{!blogMetaData.youtubeHero && blogMetaData.imgThumb && imageUrl && (
<div className="col-span-12 xl:row-start-2 xl:col-start-1 xl:col-span-9 mb-6">
<div className="relative w-full aspect-[1.91/1] overflow-hidden rounded-lg border border-foreground/10">
<Image
src={imageUrl}
alt={blogMetaData.title}
fill
quality={100}
sizes="(max-width: 768px) 100vw, 75vw"
className="object-cover"
/>
</div>
</div>
)}
{/* Row 3: Article */}
<div className="col-span-12 xl:row-start-3 xl:col-start-2 xl:col-span-7 pb-16">
<article>
<div className="prose prose-docs">
{blogMetaData.youtubeHero ? (
<iframe
title="YouTube video player"
className="w-full"
width="700"
height="350"
src={blogMetaData.youtubeHero}
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen={true}
/>
) : null}
{mdxRendered}
</div>
</article>
{isLaunchWeek7 && <BlogLinks />}
{isLaunchWeekX && <LWXSummary />}
{isGAWeek && <LW11Summary />}
{isLaunchWeek12 && <LW12Summary />}
{isLaunchWeek13 && <LW13Summary />}
{isLaunchWeek14 && <LW14Summary />}
{isLaunchWeek15 && <LW15Summary />}
<div className="block lg:hidden py-8">
<div className="text-foreground-lighter text-sm">Share this article</div>
<ShareArticleActions title={blogMetaData.title} slug={blogMetaData.slug} />
</div>
<div className="grid gap-8 py-8 lg:grid-cols-1">
<div>
{prevPost && (
<NextCard
post={
prevPost as unknown as {
path: string
title: string
formattedDate: string
}
}
label="Previous post"
/>
)}
</div>
<div>
{nextPost && (
<NextCard
post={
nextPost as unknown as {
path: string
title: string
formattedDate: string
}
}
label="Next post"
className="text-right"
/>
)}
</div>
</div>
<div className="grid grid-cols-12 lg:gap-16 xl:gap-8">
{/* Content */}
<div className="col-span-12 lg:col-span-7 xl:col-span-7">
<article>
<div className={['prose prose-docs'].join(' ')}>
{blogMetaData.youtubeHero ? (
<iframe
title="YouTube video player"
className="w-full"
width="700"
height="350"
src={blogMetaData.youtubeHero}
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen={true}
/>
) : (
imageUrl && (
<div className="hidden md:block relative mb-8 w-full aspect-[1.91/1] overflow-auto rounded-lg border">
<Image
src={imageUrl}
alt={blogMetaData.title}
fill
sizes={BLOG_POST_HERO_IMAGE_SIZES}
className="object-cover m-0"
/>
</div>
)
)}
{mdxRendered}
</div>
</article>
{isLaunchWeek7 && <BlogLinks />}
{isLaunchWeekX && <LWXSummary />}
{isGAWeek && <LW11Summary />}
{isLaunchWeek12 && <LW12Summary />}
{isLaunchWeek13 && <LW13Summary />}
{isLaunchWeek14 && <LW14Summary />}
{isLaunchWeek15 && <LW15Summary />}
<div className="block lg:hidden py-8">
<div className="text-foreground-lighter text-sm">Share this article</div>
<ShareArticleActions title={blogMetaData.title} slug={blogMetaData.slug} />
</div>
<div className="grid gap-8 py-8 lg:grid-cols-1">
<div>
{prevPost && (
<NextCard
post={
prevPost as unknown as {
path: string
title: string
formattedDate: string
}
}
label="Previous post"
/>
)}
</div>
<div>
{nextPost && (
<NextCard
post={
nextPost as unknown as {
path: string
title: string
formattedDate: string
}
}
label="Next post"
className="text-right"
/>
)}
</div>
</div>
</div>
{/* Sidebar */}
<div className="relative col-span-12 space-y-8 lg:col-span-5 xl:col-span-3 xl:col-start-9">
<div className="space-y-6">
<div className="hidden lg:block">
<div className="flex flex-wrap gap-2">
{(blogMetaData.tags as Array<Tag>)?.map((tag) => {
const tagName = typeof tag === 'string' ? tag : tag.name
const tagId = typeof tag === 'string' ? tag : tag.id.toString()
return (
<Link href={`/blog/tags/${tagName}`} key={`category-badge-${tagId}`}>
<Badge>{tagName}</Badge>
</Link>
)
})}
</div>
</div>
<div className="hidden lg:block">{toc}</div>
<div className="hidden lg:block">
<div className="text-foreground text-sm">Share this article</div>
<ShareArticleActions title={blogMetaData.title} slug={blogMetaData.slug} />
</div>
</div>
</div>
{/* TOC: starts at row 2 alongside image, sticky through article */}
<div className="hidden xl:block xl:row-start-2 xl:row-span-2 xl:col-start-10 xl:col-span-3 xl:pl-6">
<div className="sticky top-24 flex flex-col gap-6 max-h-[calc(100vh-7rem)]">
<div className="overflow-y-auto min-h-0 flex-1">{toc}</div>
<div className="shrink-0">
<div className="text-foreground text-sm mb-2">Share this article</div>
<ShareArticleActions title={blogMetaData.title} slug={blogMetaData.slug} />
</div>
</div>
</div>
</div>
</div>
<CTABanner />
</DefaultLayout>
<div className="border-t">
<CTASection />
</div>
</div>
</>
)
}
@@ -0,0 +1,48 @@
'use client'
import { setBlogViewCookie, type BlogView } from 'app/blog/blog-view'
import { motion } from 'framer-motion'
import { AlignJustify, Grid } from 'lucide-react'
import { cn } from 'ui'
export default function BlogViewToggle({
view,
setView,
}: {
view: BlogView
setView: (view: BlogView) => void
}) {
return (
<div className="flex items-center border border-border rounded-md p-0.5 gap-0.5 bg-surface-100">
{(['list', 'grid'] as BlogView[]).map((v) => (
<button
key={v}
onClick={() => {
setView(v)
setBlogViewCookie(v)
}}
aria-label={`${v} view`}
className={cn(
'relative flex items-center justify-center w-7 h-7 rounded-sm transition-colors',
view === v ? 'text-foreground' : 'text-foreground-light hover:text-foreground'
)}
>
{view === v && (
<motion.span
layoutId="blog-view-bg"
className="absolute inset-0 rounded-sm bg-surface-300 border border-border"
transition={{ type: 'spring', duration: 0.3, bounce: 0.15 }}
/>
)}
<span className="relative z-10">
{v === 'list' ? (
<AlignJustify className="w-3.5 h-3.5" />
) : (
<Grid className="w-3.5 h-3.5" />
)}
</span>
</button>
))}
</div>
)
}
+24 -44
View File
@@ -1,6 +1,7 @@
import Image from 'next/image'
import Link from 'next/link'
import AuthorAvatars from './AuthorAvatars'
import authors from '@/lib/authors.json'
import {
BLOG_FEATURED_IMAGE_SIZES,
@@ -10,7 +11,6 @@ import {
import type PostTypes from '@/types/post'
function FeaturedThumb(blog: PostTypes) {
// For static posts, look up author info from authors.json
const authorArray = blog.author?.split(',').map((a) => a.trim()) || []
const author = []
@@ -30,65 +30,45 @@ function renderFeaturedThumb(blog: PostTypes, author: any[]) {
const imageUrl = getBlogThumbnailImage(blog) ?? BLOG_PLACEHOLDER_IMAGE
return (
<div key={blog.slug} className="w-full">
<div key={blog.slug} className="w-full grid lg:grid-cols-12 gap-8">
{/* Image */}
<Link
href={`${blog.path}`}
className="grid gap-4 lg:grid-cols-7 lg:gap-8 xl:gap-12 hover:bg-surface-200 dark:hover:bg-surface-75 p-2 sm:p-4 rounded-xl"
prefetch={false}
className="relative w-full aspect-[1.91/1] lg:col-span-6 overflow-hidden block group"
>
<div className="relative w-full aspect-2/1 lg:col-span-3 lg:aspect-3/2 overflow-auto rounded-lg border">
<div className="relative w-full h-full shadow-lg border border-foreground/10 rounded-lg overflow-hidden">
<Image
src={imageUrl}
fill
sizes={BLOG_FEATURED_IMAGE_SIZES}
priority
className="object-cover bg-alternative"
className="object-cover bg-alternative group-hover:scale-[1.02] transition-transform duration-300"
alt="blog thumbnail"
/>
</div>
<div className="flex flex-col space-y-2 lg:col-span-4 xl:justify-center max-w-xl">
<div className="text-lighter flex space-x-2 text-sm">
</Link>
{/* Text */}
<div className="flex flex-col lg:col-span-6 px-6 pb-8">
<div>
<Link href={`${blog.path}`} prefetch={false} className="group">
<h2 className="h2 lg:!text-xl xl:!text-2xl !mb-2 group-hover:underline">
{blog.title}
</h2>
</Link>
<p className="p">{blog.description}</p>
</div>
<div className="flex items-center justify-between mt-4">
<AuthorAvatars authors={author} size="md" />
<div className="text-foreground-lighter flex space-x-2 text-sm">
<span>{blog.formattedDate}</span>
<span>·</span>
<span>{blog.readingTime}</span>
</div>
<div>
<h2 className="h2 lg:text-2xl! xl:text-3xl! mb-2!">{blog.title}</h2>
<p className="p xl:text-lg">{blog.description}</p>
</div>
<div className="flex flex-col w-max gap-2">
{author.filter(Boolean).map((author: any, i: number) => {
const authorImageUrl =
typeof author.author_image_url === 'string'
? author.author_image_url
: (author.author_image_url as { url: string })?.url || ''
return (
<div
className="flex items-center space-x-2"
key={`author-feat-${i}-${author.author}`}
>
{imageUrl && (
<div className="relative h-6 w-6 overflow-auto">
<Image
src={authorImageUrl}
alt={`${author.author} avatar`}
className="rounded-full object-cover"
fill
sizes="30px"
/>
</div>
)}
<div className="flex flex-col">
<span className="text-foreground m-0 text-sm">{author.author}</span>
</div>
</div>
)
})}
</div>
</div>
</Link>
</div>
</div>
)
}
+2 -2
View File
@@ -16,12 +16,12 @@ const BrowserFrame: React.FC<Props> = ({
return (
<div
className={cn(
'relative rounded-2xl shadow-lg p-2 w-full h-full bg-alternative-200 border flex flex-col',
'relative rounded-2xl shadow-lg py-2 w-full h-full bg-alternative-200 border flex flex-col',
className
)}
>
{hasFrameButtons && (
<div className="w-full px-2 pt-1 pb-3 relative flex items-center gap-1.5 lg:gap-2">
<div className="w-full px-4 pt-1 pb-3 relative flex items-center gap-1 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" />
@@ -1,7 +1,7 @@
import 'swiper/css'
import React, { FC } from 'react'
import { useRouter } from 'next/router'
import { useRouter } from 'next/compat/router'
import { Swiper, SwiperSlide } from 'swiper/react'
import ExampleCard from '../ExampleCard'
@@ -14,7 +14,8 @@ interface Props {
}
const ExamplesMobile: FC<Props> = ({ examples, className }: any) => {
const { basePath } = useRouter()
const router = useRouter()
const basePath = router?.basePath ?? ''
return (
<div className={className}>
@@ -1,10 +1,9 @@
import { Button, cn, IconGitHubSolid } from 'ui'
import Link from 'next/link'
import SectionContainer from '~/components/Layouts/SectionContainer'
import ExampleCard from '../ExampleCard'
import Examples from 'data/Examples'
import Link from 'next/link'
import { Button, cn, IconGitHubSolid } from 'ui'
import ExampleCard from '../ExampleCard'
import ExamplesMobile from './ExamplesMobile'
const BuiltWithSupabase = () => {
@@ -33,7 +32,7 @@ const BuiltWithSupabase = () => {
</div>
</div>
</SectionContainer>
<SectionContainer className="relative w-full px-0! lg:px-16! xl:px-20! pb-0! mb-16 md:mb-12 lg:mb-12 pt-6! max-w-[1400px]">
<SectionContainer className="relative w-full !px-6 !pb-0 mb-16 md:mb-12 lg:mb-12 !pt-6 !max-w-[var(--container-max-w,75rem)]">
<ExamplesMobile examples={Examples.slice(0, 6)} className="lg:hidden" />
<div className="hidden lg:grid grid-cols-12 gap-5 mt-4">
{Examples.slice(0, 6).map((example, i) => {
@@ -19,7 +19,7 @@ const CutomsersSliderDesktop = dynamic(() => import('./CutomsersSliderDesktop'))
const CustomerStories = () => (
<div id="customers" className="overflow-hidden pb-16 md:pb-24">
<SectionContainer className="pb-8! w-full flex gap-4 justify-between flex-col xl:flex-row xl:items-end">
<SectionContainer className="!pb-16 w-full flex gap-4 justify-between flex-col xl:flex-row xl:items-end !max-w-[var(--container-max-w,75rem)] !px-6">
<SectionHeader
title="Trusted by the world’s"
title_alt=" most innovative companies."
@@ -36,6 +36,7 @@ const CustomerStories = () => (
</Button>
</div>
</SectionContainer>
<CustomersSliderMobile columns={compositionCols} className="md:hidden w-full h-[230px]" />
<CutomsersSliderDesktop columns={compositionCols} className="hidden md:flex" />
</div>
+92 -28
View File
@@ -1,6 +1,5 @@
'use client'
import { CheckIcon } from '@heroicons/react/outline'
import { REALTIME_CHANNEL_STATES } from '@supabase/supabase-js'
import SupabaseWordmark from '~/components/Nav/SupabaseWordmark'
import supabase from '~/lib/supabase'
@@ -25,6 +24,84 @@ import { ThemeToggle } from 'ui-patterns/ThemeToggle'
import useDarkLaunchWeeks from '../../hooks/useDarkLaunchWeeks'
import SectionContainer from '../Layouts/SectionContainer'
function Soc2Seal({ className }: { className?: string }) {
return (
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="50" cy="50" r="48" stroke="currentColor" strokeWidth="2" />
<circle cx="50" cy="50" r="42" stroke="currentColor" strokeWidth="1" />
<text
x="50"
y="34"
textAnchor="middle"
fill="currentColor"
fontSize="10"
fontWeight="500"
fontFamily="system-ui, sans-serif"
letterSpacing="3"
>
AICPA
</text>
<text
x="50"
y="58"
textAnchor="middle"
fill="currentColor"
fontSize="24"
fontWeight="800"
fontFamily="system-ui, sans-serif"
>
SOC2
</text>
<text
x="50"
y="72"
textAnchor="middle"
fill="currentColor"
fontSize="10"
fontWeight="500"
fontFamily="system-ui, sans-serif"
letterSpacing="3"
>
TYPE 2
</text>
</svg>
)
}
function HipaaSeal({ className }: { className?: string }) {
return (
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="50" cy="50" r="48" stroke="currentColor" strokeWidth="2" />
<circle cx="50" cy="50" r="42" stroke="currentColor" strokeWidth="1" />
<text
x="50"
y="50"
textAnchor="middle"
fill="currentColor"
fontSize="16"
fontWeight="700"
fontFamily="system-ui, sans-serif"
letterSpacing="1"
>
HIPAA
</text>
<text
x="50"
y="64"
textAnchor="middle"
fill="currentColor"
fontSize="7.5"
fontWeight="500"
fontFamily="system-ui, sans-serif"
letterSpacing="1"
>
COMPLIANT
</text>
</svg>
)
}
interface Props {
className?: string
hideFooter?: boolean
@@ -85,31 +162,6 @@ const Footer = (props: Props) => {
<h2 id="footerHeading" className="sr-only">
Footer
</h2>
<div className="w-full py-0!">
<SectionContainer className="grid grid-cols-2 md:flex items-center justify-between text-foreground md:justify-center gap-8 md:gap-16 xl:gap-28 py-6! md:py-10! text-sm">
<div className="flex flex-col md:flex-row gap-2 md:items-center">
We protect your data.
<Link href="/security" className="text-brand-link hover:underline">
More on Security
</Link>
</div>
<ul className="flex flex-col md:flex-row gap-2 md:gap-8 justify-center md:items-center">
<li className="flex items-center gap-2 whitespace-nowrap flex-nowrap">
<CheckIcon className="w-4 h-4" /> SOC2 Type 2{' '}
<span className="text-foreground-lighter hidden sm:inline">Certified</span>
</li>
<li className="flex items-center gap-2 whitespace-nowrap flex-nowrap">
<CheckIcon className="w-4 h-4" /> HIPAA{' '}
<span className="text-foreground-lighter hidden sm:inline">Compliant</span>
</li>
<li className="flex items-center gap-2 whitespace-nowrap flex-nowrap">
<CheckIcon className="w-4 h-4" /> ISO 27001{' '}
<span className="text-foreground-lighter hidden sm:inline">Certified</span>
</li>
</ul>
</SectionContainer>
<div className="w-full h-px bg-linear-to-r from-transparent via-border to-transparent" />
</div>
<SectionContainer className="py-8">
<div className="xl:grid xl:grid-cols-7 xl:gap-4">
<div className="xl:col-span-2 flex flex-col gap-8">
@@ -249,8 +301,20 @@ const Footer = (props: Props) => {
</div>
</div>
</div>
<div className="border-default mt-32 flex justify-between border-t pt-8">
<small className="small">&copy; Supabase Inc</small>
<div className="border-muted mt-32 flex flex-wrap items-center justify-between gap-4 border-t pt-8">
<div className="flex flex-wrap items-center gap-3">
<small className="small">&copy; Supabase Inc</small>
<span className="text-foreground-muted text-xs">·</span>
<span className="flex items-center gap-1.5 text-foreground-muted text-xs">
<Soc2Seal className="w-5 h-5 shrink-0" />
SOC2 Type 2
</span>
<span className="text-foreground-muted text-xs">·</span>
<span className="flex items-center gap-1.5 text-foreground-muted text-xs">
<HipaaSeal className="w-5 h-5 shrink-0" />
HIPAA Compliant
</span>
</div>
<div className={cn(forceDark && 'hidden')}>
<ThemeToggle forceDark={forceDark} />
</div>
@@ -1,7 +1,7 @@
'use client'
import classNames from 'classnames'
import { Ref, forwardRef } from 'react'
import { forwardRef, Ref } from 'react'
interface Props {
children: React.ReactNode
@@ -19,7 +19,7 @@ const SectionContainer = forwardRef(
ref={ref}
id={id}
className={classNames(
`sm:py-18 container relative mx-auto px-6 py-16 md:py-24 lg:px-16 lg:py-24 xl:px-20`,
`sm:py-18 relative mx-auto w-full max-w-[var(--container-max-w,75rem)] px-6 py-16 md:py-24 lg:py-24`,
className
)}
>
@@ -1,11 +1,11 @@
import { Ref, forwardRef } from 'react'
import { cn } from 'ui'
import { cva, type VariantProps } from 'class-variance-authority'
import { forwardRef, Ref } from 'react'
import { cn } from 'ui'
const sectionContainerVariants = cva('max-w-7xl relative mx-auto px-6', {
const sectionContainerVariants = cva('relative mx-auto w-full px-6', {
variants: {
width: {
normal: 'lg:px-16 xl:px-20',
normal: 'max-w-[var(--container-max-w,75rem)]',
smallScreenFull: 'max-w-full lg:container px-0',
},
height: {
+2 -2
View File
@@ -112,8 +112,8 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
isTransparent && 'border-transparent backdrop-blur-none'
)}
>
<div className="relative flex justify-between h-16 mx-auto lg:container lg:px-16 xl:px-20">
<div className="flex items-center px-6 lg:px-0 flex-1 sm:items-stretch justify-between">
<div className="relative flex justify-between h-16 mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="flex items-center flex-1 sm:items-stretch justify-between">
<div className="flex items-center">
<div className="flex items-center shrink-0">
<RightClickBrandLogo />
+125 -32
View File
@@ -1,36 +1,129 @@
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-sm 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" />
@@ -78,20 +78,24 @@ const Metrics = ({ isActive }: { isActive?: boolean }) => {
return (
<div
className={cn(
'absolute inset-0 bottom-4 px-4 opacity-50 transition-opacity overflow-hidden',
'absolute inset-0 opacity-50 transition-opacity overflow-hidden',
isActive && 'opacity-100'
)}
>
<div className="relative rounded-md w-full h-full border border-overlay shadow-sm p-4 min-w-[300px]!">
<p className="text-foreground text-sm mb-2">Execution time</p>
<p className="text-foreground text-base mb-4">
{displayValue ? data[displayValue]?.pv : highlightedValue}ms {displayValue ? '' : '(Avg)'}
</p>
<ResponsiveContainer minWidth={200} minHeight={200} width="100%" height="90%">
<div className="relative w-full h-full py-4 !min-w-[300px]">
<div className="px-6 mb-4">
<p className="text-foreground text-sm mb-2">Execution time</p>
<p className="text-foreground text-base">
{displayValue ? data[displayValue]?.pv : highlightedValue}ms{' '}
{displayValue ? '' : '(Avg)'}
</p>
</div>
<ResponsiveContainer minWidth={200} width="100%" height="70%">
<AreaChart
className="relative z-20 text-xs"
data={data}
margin={{ top: 10, right: 0, left: 0, bottom: 40 }}
margin={{ top: 10, right: 0, left: 0, bottom: 0 }}
onMouseMove={(e: any) => {
if (e.activeTooltipIndex !== displayValue) {
setDisplayValue(e.activeTooltipIndex)
@@ -125,8 +129,9 @@ const Metrics = ({ isActive }: { isActive?: boolean }) => {
/>
</AreaChart>
</ResponsiveContainer>
{data && (
<div className="absolute inset-4 top-auto text-foreground-lighter flex items-center justify-between text-xs">
<div className="absolute bottom-4 inset-x-0 px-6 text-foreground-lighter flex items-center justify-between text-xs">
<span>{dayjs(data[0].timestamp).subtract(7, 'days').format(dateFormat)}</span>
<span>{dayjs(data[data?.length - 1]?.timestamp).format(dateFormat)}</span>
</div>
Loaded 100 of 169 files, more files were not shown because too many files have changed in this diff. Show more