mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
new layout
This commit is contained in:
1 parent
7d765a474e
commit
ece51b4d2f
17 files changed
+477
-148
No files matched your search
@@ -99,28 +99,26 @@ export function BuiltWithSupabaseSection() {
|
||||
const gridExamples = Examples.slice(2, 6)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="py-24 flex flex-col gap-16">
|
||||
{/* Header row */}
|
||||
<div className="border-b border-border">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
|
||||
<div className="flex items-end justify-between pt-40 pb-8">
|
||||
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
|
||||
Kickstart your next project <br />
|
||||
<span className="text-foreground">with production ready templates</span>
|
||||
</h3>
|
||||
<Link
|
||||
href="/docs/guides/examples"
|
||||
className="text-sm text-foreground-light hover:text-foreground underline"
|
||||
>
|
||||
View all examples
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] w-full px-6">
|
||||
<div className="flex items-end 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>
|
||||
|
||||
{/* Featured row - 2 large cells */}
|
||||
<div className="border-b border-border max-w-[var(--container-max-w,75rem)] mx-auto">
|
||||
<div className=" border-x border-border">
|
||||
<div className="border border-border rounded-md overflow-clip mx-auto max-w-[var(--container-max-w,75rem)]">
|
||||
{/* Featured row - 2 large cells */}
|
||||
<div className="border-b border-border">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2">
|
||||
{featuredExamples.map((example) => {
|
||||
const logo = EXAMPLE_LOGO[example.title]
|
||||
@@ -167,36 +165,36 @@ export function BuiltWithSupabaseSection() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid row - 4 smaller cells */}
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{gridExamples.map((example) => {
|
||||
const logo = EXAMPLE_LOGO[example.title]
|
||||
return (
|
||||
<Link
|
||||
key={example.title}
|
||||
href={example.repo_url ?? '#'}
|
||||
target="_blank"
|
||||
className="group flex flex-col border-b sm:border-b-0 sm:border-r border-border last:border-r-0 last:border-b-0 px-6 py-10 hover:bg-surface-75/50 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>
|
||||
)
|
||||
})}
|
||||
{/* Grid row - 4 smaller cells */}
|
||||
<div className="">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{gridExamples.map((example) => {
|
||||
const logo = EXAMPLE_LOGO[example.title]
|
||||
return (
|
||||
<Link
|
||||
key={example.title}
|
||||
href={example.repo_url ?? '#'}
|
||||
target="_blank"
|
||||
className="group flex flex-col border-b sm:border-b-0 sm:border-r border-border last:border-r-0 last:border-b-0 px-6 py-10 hover:bg-surface-75/50 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>
|
||||
</div>
|
||||
|
||||
@@ -152,8 +152,8 @@ export function CTASection() {
|
||||
const contributors = useContributors()
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden pt-32 pb-64">
|
||||
<AvatarCloud contributors={contributors} />
|
||||
<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>
|
||||
|
||||
@@ -7,8 +7,18 @@ 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)
|
||||
|
||||
@@ -24,9 +34,11 @@ export function CommunitySectionV2() {
|
||||
return topTweets[idx]
|
||||
})
|
||||
|
||||
const columns = distributeToColumns(currentTweets, COLS)
|
||||
|
||||
return (
|
||||
<div className="border-b border-border">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x 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">
|
||||
@@ -53,29 +65,36 @@ export function CommunitySectionV2() {
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
<motion.div
|
||||
key={page}
|
||||
className="columns-1 sm:columns-2 md:columns-3 lg:columns-4 gap-2 -mx-4"
|
||||
className="grid grid-cols-2 md:grid-cols-5 gap-2 -mx-4"
|
||||
>
|
||||
{currentTweets.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: i * 0.03,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
}}
|
||||
className="break-inside-avoid mb-2"
|
||||
{columns.map((col, colIdx) => (
|
||||
<div
|
||||
key={colIdx}
|
||||
className="flex flex-col gap-2"
|
||||
style={{ transform: `translateY(${COL_OFFSETS[colIdx]}px)` }}
|
||||
>
|
||||
<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>
|
||||
{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>
|
||||
|
||||
@@ -52,8 +52,7 @@ const aiBuilderStories = [
|
||||
{
|
||||
name: 'Bolt',
|
||||
logo: '/images/logos/publicity/bolt.svg',
|
||||
description:
|
||||
'We store embeddings in a PostgreSQL database, hosted by Supabase, to perform a similarity search to identify',
|
||||
description: 'We store embeddings in a PostgreSQL database, hosted by Supabase.',
|
||||
slug: 'bolt',
|
||||
gradient: 'linear-gradient(to top, #0a1a3a 0%, #1a3f6f 30%, #2563eb 65%, #60a5fa 100%)',
|
||||
},
|
||||
@@ -61,7 +60,7 @@ const aiBuilderStories = [
|
||||
name: 'Lovable',
|
||||
logo: '/images/logos/publicity/lovable.svg',
|
||||
description:
|
||||
'We store embeddings in a PostgreSQL database, hosted by Supabase, to perform a similarity search to identify',
|
||||
'We store embeddings in a PostgreSQL database. Supabase is a great partner for us as we scale.',
|
||||
slug: 'lovable',
|
||||
gradient:
|
||||
'linear-gradient(to bottom, #c4b5fd 0%, #a78bfa 20%, #ec4899 50%, #fb7185 75%, #ef4444 100%)',
|
||||
@@ -73,11 +72,11 @@ export function CustomerStoriesSection() {
|
||||
const active = customerStories[activeIdx]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="py-24 flex flex-col gap-16 border-b border-border">
|
||||
{/* Header row */}
|
||||
<div className="border-b border-border">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
|
||||
<div className="flex items-end justify-between pt-40 pb-8">
|
||||
<div className="">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
|
||||
<div className="flex items-end 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>
|
||||
@@ -93,8 +92,8 @@ export function CustomerStoriesSection() {
|
||||
</div>
|
||||
|
||||
{/* Cards row */}
|
||||
<div className="border-b border-border mx-auto max-w-[var(--container-max-w,75rem)]">
|
||||
<div className="border-x border-border">
|
||||
<div className="border border-border rounded-md overflow-clip mx-auto max-w-[var(--container-max-w,75rem)]">
|
||||
<div className="">
|
||||
<motion.div
|
||||
className="grid min-h-[480px]"
|
||||
initial={false}
|
||||
@@ -158,11 +157,11 @@ export function CustomerStoriesSection() {
|
||||
</div>
|
||||
|
||||
{/* AI Builder stories */}
|
||||
<div className="border-b border-border">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-8 py-20">
|
||||
<div className="">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[420px_1fr] gap-8">
|
||||
<div className="flex flex-col min-h-[320px] gap-4 justify-between">
|
||||
<h3 className="text-2xl md:text-4xl text-foreground-lighter">
|
||||
<h3 className="text-xl md:text-3xl text-foreground-lighter">
|
||||
Powering the next wave
|
||||
<br />
|
||||
<span className="text-foreground">of AI builders</span>
|
||||
|
||||
@@ -57,7 +57,7 @@ export function DashboardFeaturesSection({
|
||||
<div className="border-b border-border">
|
||||
<div
|
||||
ref={sectionRef}
|
||||
className="relative mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border pt-10 pb-4 overflow-hidden"
|
||||
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>
|
||||
@@ -105,7 +105,7 @@ export function DashboardFeaturesSection({
|
||||
|
||||
{/* Customer story banner */}
|
||||
<div className="border-t border-border">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] py-4">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={story.slug}
|
||||
|
||||
@@ -190,6 +190,35 @@ const { data: todos } = await useAsyncData(
|
||||
},
|
||||
]
|
||||
|
||||
// ── Example links per framework ──────────────────────────────────────────────
|
||||
|
||||
const frameworkExamples: Record<string, { title: string; description: string; url: string }[]> = {
|
||||
React: [
|
||||
{ title: 'Todo List', description: 'CRUD app with real-time and RLS.', url: '/docs/guides/getting-started/tutorials/with-react' },
|
||||
{ title: 'Auth & User Management', description: 'Email, social login, and user profiles.', url: '/docs/guides/auth/quickstarts/react' },
|
||||
],
|
||||
'Next.js': [
|
||||
{ title: 'App Router Quickstart', description: 'Server Components with SSR auth.', url: '/docs/guides/getting-started/quickstarts/nextjs' },
|
||||
{ title: 'Auth with PKCE', description: 'Secure auth flow with middleware.', url: '/docs/guides/auth/server-side/nextjs' },
|
||||
],
|
||||
Flutter: [
|
||||
{ title: 'User Management', description: 'Auth and profile management.', url: '/docs/guides/getting-started/tutorials/with-flutter' },
|
||||
{ title: 'Deep Linking', description: 'OAuth and magic link for mobile.', url: '/docs/guides/auth/native-mobile-deep-linking' },
|
||||
],
|
||||
Svelte: [
|
||||
{ title: 'Todo List', description: 'Server-side auth and real-time data.', url: '/docs/guides/getting-started/tutorials/with-svelte' },
|
||||
{ title: 'Auth Helpers', description: 'Sessions and protected routes.', url: '/docs/guides/auth/server-side/sveltekit' },
|
||||
],
|
||||
Vue: [
|
||||
{ title: 'Todo List', description: 'CRUD with Vue 3 Composition API.', url: '/docs/guides/getting-started/tutorials/with-vue-3' },
|
||||
{ title: 'Auth with Vue', description: 'Email and OAuth authentication.', url: '/docs/guides/getting-started/quickstarts/vuejs' },
|
||||
],
|
||||
Nuxt: [
|
||||
{ title: 'Nuxt Module', description: 'Auto-configured auth and client.', url: '/docs/guides/getting-started/quickstarts/nuxtjs' },
|
||||
{ title: 'Server Routes', description: 'Service role access from server.', url: '/docs/guides/auth/server-side/nuxt' },
|
||||
],
|
||||
}
|
||||
|
||||
// ── Server component: pre-highlights all code ───────────────────────────────
|
||||
|
||||
export async function FrameworksSection() {
|
||||
@@ -204,6 +233,7 @@ export async function FrameworksSection() {
|
||||
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()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
type Framework = {
|
||||
@@ -11,18 +11,26 @@ type Framework = {
|
||||
docsUrl: string
|
||||
darkHtml: string
|
||||
lightHtml: string
|
||||
examples: { title: string; description: string; url: string }[]
|
||||
}
|
||||
|
||||
export function FrameworksSectionClient({ frameworks }: { frameworks: Framework[] }) {
|
||||
const [activeIdx, setActiveIdx] = useState(0)
|
||||
const prevIdx = useRef(0)
|
||||
const direction = activeIdx > prevIdx.current ? -1 : 1
|
||||
const active = frameworks[activeIdx]
|
||||
|
||||
const handleTabChange = (index: number) => {
|
||||
prevIdx.current = activeIdx
|
||||
setActiveIdx(index)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-border">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] pl-6 border-x border-border">
|
||||
<div className="border-b border-border py-24">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] pl-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0">
|
||||
{/* Left: title */}
|
||||
<div className="flex items-center py-16 lg:py-24 pr-6">
|
||||
{/* Left: title + example cards */}
|
||||
<div className="flex flex-col justify-between py-4 gap-10">
|
||||
<div className="text-2xl md:text-4xl text-foreground-lighter">
|
||||
Use Supabase with{' '}
|
||||
<span className="block">
|
||||
@@ -39,16 +47,57 @@ export function FrameworksSectionClient({ frameworks }: { frameworks: Framework[
|
||||
</AnimatePresence>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{active.examples.length > 0 && (
|
||||
<AnimatePresence mode="wait" initial={false} custom={direction}>
|
||||
<motion.div
|
||||
key={active.name}
|
||||
custom={direction}
|
||||
initial="enter"
|
||||
animate="center"
|
||||
exit="exit"
|
||||
variants={{
|
||||
enter: (d: number) => ({ opacity: 0, x: -28 * d, filter: 'blur(2px)' }),
|
||||
center: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
filter: 'blur(0px)',
|
||||
transition: { duration: 0.2, delay: 0.05 },
|
||||
},
|
||||
exit: (d: number) => ({
|
||||
opacity: 0,
|
||||
x: 28 * d,
|
||||
filter: 'blur(2px)',
|
||||
transition: { duration: 0.1 },
|
||||
}),
|
||||
}}
|
||||
className="flex flex-col gap-2.5 max-w-sm"
|
||||
>
|
||||
{active.examples.map((example) => (
|
||||
<Link
|
||||
key={example.url}
|
||||
href={example.url}
|
||||
className="group block border border-border rounded-lg px-2.5 py-2 hover:bg-surface-200 transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-foreground">{example.title}</span>
|
||||
<span className="block text-xs text-foreground-lighter mt-0.5 truncate">
|
||||
{example.description}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: icon tabs + code */}
|
||||
<div className="border-l border-border flex flex-col">
|
||||
<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={() => setActiveIdx(index)}
|
||||
onClick={() => handleTabChange(index)}
|
||||
className={cn(
|
||||
'flex items-center justify-center py-4 border-r border-border last:border-r-0 transition-colors',
|
||||
index === activeIdx
|
||||
|
||||
@@ -1,25 +1,80 @@
|
||||
'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 = 7
|
||||
const GAP = 1
|
||||
const COLS = 16
|
||||
const ROWS = 12
|
||||
const CELL = 3
|
||||
const GAP = 0.5
|
||||
const COLS = 72
|
||||
const ROWS = 40
|
||||
|
||||
const LEVELS = [
|
||||
'hsl(var(--background-surface-300))',
|
||||
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-400))',
|
||||
'hsl(var(--brand-500))',
|
||||
'hsl(var(--brand-600))',
|
||||
'hsl(var(--brand-default))',
|
||||
'hsl(var(--brand-default))',
|
||||
]
|
||||
|
||||
function makeGrid(): number[][] {
|
||||
function makeGrid(): { level: number; isText: boolean }[][] {
|
||||
let seed = 0x9e3779b9
|
||||
const rand = () => {
|
||||
seed ^= seed << 13
|
||||
@@ -27,10 +82,14 @@ function makeGrid(): number[][] {
|
||||
seed ^= seed << 5
|
||||
return (seed >>> 0) / 0xffffffff
|
||||
}
|
||||
return Array.from({ length: COLS }, () =>
|
||||
Array.from({ length: ROWS }, () => {
|
||||
return Array.from({ length: COLS }, (_, c) =>
|
||||
Array.from({ length: ROWS }, (_, r) => {
|
||||
const v = rand()
|
||||
return v < 0.45 ? 0 : v < 0.65 ? 1 : v < 0.8 ? 2 : v < 0.92 ? 3 : 4
|
||||
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 }
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -40,23 +99,31 @@ 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="xMidYMid slice"
|
||||
className="absolute inset-0 h-full w-full opacity-70"
|
||||
preserveAspectRatio="xMaxYMid slice"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
aria-hidden
|
||||
>
|
||||
{GRID.map((col, c) =>
|
||||
col.map((level, r) => (
|
||||
col.map(({ level, isText }, r) => (
|
||||
<rect
|
||||
key={`${c}-${r}`}
|
||||
x={c * (CELL + GAP)}
|
||||
y={r * (CELL + GAP)}
|
||||
width={CELL}
|
||||
height={CELL}
|
||||
rx={2}
|
||||
fill={LEVELS[level]}
|
||||
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`,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -69,17 +136,28 @@ function ContribGraph() {
|
||||
export function OpenSourceSection() {
|
||||
const sendTelemetryEvent = useSendTelemetryEvent()
|
||||
|
||||
const stars = '98.4K'
|
||||
const starsNum = 98400
|
||||
|
||||
return (
|
||||
<div className="border-b border-border">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] pl-6 border-x border-border">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3">
|
||||
{/* Main content — spans 2 cols */}
|
||||
<div className="col-span-1 md:col-span-2 flex flex-col justify-between gap-6 py-20 min-h-[400px]">
|
||||
<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="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 justify-between gap-6 py-10 max-w-lg">
|
||||
<div>
|
||||
<h2 className="text-4xl text-foreground text-balance">Open source from day one</h2>
|
||||
<p className="mt-4 max-w-lg text-sm leading-relaxed text-foreground-lighter">
|
||||
<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're never locked in, and always in control.
|
||||
@@ -112,22 +190,6 @@ export function OpenSourceSection() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative flex flex-col items-start justify-end gap-2 overflow-hidden bg-surface-75 p-6 border-l border-border"
|
||||
>
|
||||
<ContribGraph />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background from-15% via-background/50 via-60% to-transparent" />
|
||||
<span className="relative z-10 uppercase text-xs text-foreground-lighter">
|
||||
GitHub Stars
|
||||
</span>
|
||||
<span className="relative z-10 text-5xl font-bold text-foreground tabular-nums">
|
||||
{stars}
|
||||
</span>
|
||||
<span className="relative z-10 mt-1 text-xs text-foreground-lighter">
|
||||
Top 100 GitHub repos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'config/code-hike.scss'
|
||||
import '../styles/index.css'
|
||||
|
||||
import { APP_NAME, DEFAULT_META_DESCRIPTION } from '~/lib/constants'
|
||||
import { ktfPrima } from '~/lib/fonts'
|
||||
import { geistMono, ktfPrima } from '~/lib/fonts'
|
||||
import { Agentation } from 'agentation'
|
||||
import { Metadata } from 'next'
|
||||
import type { Viewport } from 'next'
|
||||
@@ -47,7 +47,7 @@ export const viewport: Viewport = {
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning className={ktfPrima.variable}>
|
||||
<html lang="en" suppressHydrationWarning className={`${ktfPrima.variable} ${geistMono.variable}`}>
|
||||
<body>
|
||||
<Providers>{children}</Providers>
|
||||
{process.env.NODE_ENV === 'development' && <Agentation endpoint="http://localhost:4747" />}
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
useThemeSandbox,
|
||||
} from 'common'
|
||||
import { WwwCommandMenu } from 'components/CommandMenu'
|
||||
import { FontDevtools } from 'components/FontDevtools'
|
||||
import { DevToolbar, DevToolbarProvider } from 'dev-tools'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { API_URL, IS_PROD } from 'lib/constants'
|
||||
import { themes, TooltipProvider, SonnerToaster } from 'ui'
|
||||
import { CommandProvider } from 'ui-patterns/CommandMenu'
|
||||
import { useConsentToast } from 'ui-patterns/consent'
|
||||
@@ -44,6 +45,7 @@ function Providers({ children }: { children: React.ReactNode }) {
|
||||
enabled={IS_PLATFORM}
|
||||
/>
|
||||
<DevToolbar />
|
||||
{!IS_PROD && <FontDevtools />}
|
||||
</CommandProvider>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Label_Shadcn_ as Label,
|
||||
Popover_Shadcn_ as Popover,
|
||||
PopoverContent_Shadcn_ as PopoverContent,
|
||||
PopoverTrigger_Shadcn_ as PopoverTrigger,
|
||||
Select_Shadcn_ as Select,
|
||||
SelectContent_Shadcn_ as SelectContent,
|
||||
SelectItem_Shadcn_ as SelectItem,
|
||||
SelectTrigger_Shadcn_ as SelectTrigger,
|
||||
SelectValue_Shadcn_ as SelectValue,
|
||||
} from 'ui'
|
||||
|
||||
const FONT_OPTIONS = {
|
||||
sans: [
|
||||
{ label: 'KTF Prima', value: 'var(--font-ktf-prima), ui-sans-serif, system-ui, sans-serif' },
|
||||
{ label: 'System UI', value: 'ui-sans-serif, system-ui, sans-serif' },
|
||||
{ label: 'Inter', value: 'Inter, ui-sans-serif, system-ui, sans-serif' },
|
||||
{
|
||||
label: 'Helvetica Neue',
|
||||
value: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
},
|
||||
],
|
||||
mono: [
|
||||
{ label: 'Geist Mono', value: 'var(--font-geist-mono), ui-monospace, Menlo, monospace' },
|
||||
{ label: 'System Mono', value: 'ui-monospace, Menlo, Monaco, monospace' },
|
||||
{
|
||||
label: 'JetBrains Mono',
|
||||
value: '"JetBrains Mono", ui-monospace, monospace',
|
||||
},
|
||||
],
|
||||
} as const
|
||||
|
||||
const STORAGE_KEY = 'www-font-devtools'
|
||||
const STYLE_ID = 'font-devtools-override'
|
||||
|
||||
function getStored(): { sans?: string; mono?: string } {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function applyOverrides(sans?: string, mono?: string) {
|
||||
let el = document.getElementById(STYLE_ID) as HTMLStyleElement | null
|
||||
const rules: string[] = []
|
||||
|
||||
if (sans) {
|
||||
rules.push(`:root { --font-sans: ${sans} !important; }`)
|
||||
}
|
||||
if (mono) {
|
||||
rules.push(`:root { --font-mono: ${mono} !important; }`)
|
||||
}
|
||||
|
||||
if (rules.length === 0) {
|
||||
el?.remove()
|
||||
return
|
||||
}
|
||||
|
||||
if (!el) {
|
||||
el = document.createElement('style')
|
||||
el.id = STYLE_ID
|
||||
document.head.appendChild(el)
|
||||
}
|
||||
el.textContent = rules.join('\n')
|
||||
}
|
||||
|
||||
export function FontDevtools() {
|
||||
const [sans, setSans] = useState('')
|
||||
const [mono, setMono] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const stored = getStored()
|
||||
if (stored.sans) setSans(stored.sans)
|
||||
if (stored.mono) setMono(stored.mono)
|
||||
if (stored.sans || stored.mono) {
|
||||
applyOverrides(stored.sans, stored.mono)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const update = useCallback(
|
||||
(type: 'sans' | 'mono', value: string) => {
|
||||
const nextSans = type === 'sans' ? value : sans
|
||||
const nextMono = type === 'mono' ? value : mono
|
||||
setSans(nextSans)
|
||||
setMono(nextMono)
|
||||
|
||||
applyOverrides(nextSans || undefined, nextMono || undefined)
|
||||
|
||||
const stored: Record<string, string> = {}
|
||||
if (nextSans) stored.sans = nextSans
|
||||
if (nextMono) stored.mono = nextMono
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(stored))
|
||||
},
|
||||
[sans, mono]
|
||||
)
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setSans('')
|
||||
setMono('')
|
||||
applyOverrides()
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-[99999]">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button type="outline" size="tiny" className="rounded-full px-2.5 font-mono text-xs">
|
||||
Aa
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" side="top" className="w-60 space-y-3">
|
||||
<p className="text-xs font-medium text-foreground-light uppercase tracking-wider">
|
||||
Font Devtools
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-foreground-lighter">Sans</Label>
|
||||
<Select value={sans || undefined} onValueChange={(v) => update('sans', v)}>
|
||||
<SelectTrigger size="small">
|
||||
<SelectValue placeholder="Default" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FONT_OPTIONS.sans.map((f) => (
|
||||
<SelectItem key={f.label} value={f.value}>
|
||||
{f.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-foreground-lighter">Mono</Label>
|
||||
<Select value={mono || undefined} onValueChange={(v) => update('mono', v)}>
|
||||
<SelectTrigger size="small">
|
||||
<SelectValue placeholder="Default" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FONT_OPTIONS.mono.map((f) => (
|
||||
<SelectItem key={f.label} value={f.value}>
|
||||
{f.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button type="default" size="tiny" block onClick={reset}>
|
||||
Reset to defaults
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { Geist_Mono } from 'next/font/google'
|
||||
import localFont from 'next/font/local'
|
||||
|
||||
export const geistMono = Geist_Mono({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-geist-mono',
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
export const ktfPrima = localFont({
|
||||
src: [
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DevToolbar, DevToolbarProvider } from 'dev-tools'
|
||||
import { DefaultSeo } from 'next-seo'
|
||||
import type { AppProps } from 'next/app'
|
||||
import Head from 'next/head'
|
||||
import { ktfPrima } from '~/lib/fonts'
|
||||
import { geistMono, ktfPrima } from '~/lib/fonts'
|
||||
import { useRouter } from 'next/router'
|
||||
import { SonnerToaster, themes, TooltipProvider } from 'ui'
|
||||
import { CommandProvider } from 'ui-patterns/CommandMenu'
|
||||
@@ -26,7 +26,8 @@ import MetaFaviconsPagesRouter, {
|
||||
DEFAULT_FAVICON_THEME_COLOR,
|
||||
} from 'common/MetaFavicons/pages-router'
|
||||
import { WwwCommandMenu } from '~/components/CommandMenu'
|
||||
import { API_URL, APP_NAME, DEFAULT_META_DESCRIPTION } from '~/lib/constants'
|
||||
import { FontDevtools } from '~/components/FontDevtools'
|
||||
import { API_URL, APP_NAME, DEFAULT_META_DESCRIPTION, IS_PROD } from '~/lib/constants'
|
||||
import useDarkLaunchWeeks from '../hooks/useDarkLaunchWeeks'
|
||||
import { useWwwCommandMenuTelemetry } from '../hooks/useWwwCommandMenuTelemetry'
|
||||
|
||||
@@ -54,7 +55,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={`${ktfPrima.variable} ${ktfPrima.className}`}>
|
||||
<main className={`${ktfPrima.variable} ${geistMono.variable}`}>
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</Head>
|
||||
@@ -110,6 +111,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
enabled={IS_PLATFORM}
|
||||
/>
|
||||
<DevToolbar />
|
||||
{!IS_PROD && <FontDevtools />}
|
||||
</CommandProvider>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -2766,4 +2766,4 @@
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
</rss>
|
||||
@@ -24,6 +24,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: var(--font-ktf-prima), ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: var(--font-geist-mono), ui-monospace, Menlo, monospace;
|
||||
--heading-tracking: -0.02em;
|
||||
--body-tracking: -0.01em;
|
||||
--heading-leading: 1.1;
|
||||
@@ -36,7 +38,6 @@ html {
|
||||
|
||||
body {
|
||||
@apply bg text-foreground;
|
||||
font-family: var(--font-ktf-prima), ui-sans-serif, system-ui, sans-serif;
|
||||
letter-spacing: var(--body-tracking);
|
||||
line-height: var(--body-leading);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -44,11 +45,15 @@ body {
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-ktf-prima), ui-sans-serif, system-ui, sans-serif;
|
||||
letter-spacing: var(--heading-tracking);
|
||||
line-height: var(--heading-leading);
|
||||
}
|
||||
|
||||
pre, code, kbd, samp,
|
||||
pre *, code *, kbd *, samp * {
|
||||
font-family: var(--font-mono) !important;
|
||||
}
|
||||
|
||||
/* h1,
|
||||
h2,
|
||||
h3,
|
||||
|
||||
@@ -273,7 +273,7 @@ const uiConfig = ui({
|
||||
},
|
||||
figcaption: {
|
||||
color: 'hsl(var(--foreground-muted))',
|
||||
fontFamily: 'Office Code Pro, monospace',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
},
|
||||
'figure.quote-figure p:first-child': {
|
||||
marginTop: '0 !important',
|
||||
@@ -374,8 +374,8 @@ const uiConfig = ui({
|
||||
xs: '480px',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: 'var(--font-ktf-prima), ui-sans-serif, system-ui, sans-serif',
|
||||
mono: 'var(--font-source-code-pro), Source Code Pro, Office Code Pro, Menlo, monospace',
|
||||
sans: 'var(--font-sans)',
|
||||
mono: 'var(--font-mono)',
|
||||
},
|
||||
|
||||
// shadcn defaults START
|
||||
|
||||
@@ -34,11 +34,6 @@ export function TweetCard(props: TweetCard) {
|
||||
<div className="w-6" />
|
||||
)}
|
||||
<p className="text-foreground text-sm font-medium">{props.handle}</p>
|
||||
<div className="absolute -left-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black">
|
||||
<svg className="h-[12px] w-[12px]" fill="white" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user