new layout

This commit is contained in:
Alan Daniel committed 2026-03-17 13:32:45 -04:00
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&apos;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>
+2 -2
View File
@@ -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" />}
+3 -1
View File
@@ -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>
+161
View File
@@ -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>
)
}
+7
View File
@@ -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: [
{
+5 -3
View File
@@ -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>
+1 -1
View File
@@ -2766,4 +2766,4 @@
</item>
</channel>
</rss>
</rss>
+7 -2
View File
@@ -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,
+3 -3
View File
@@ -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>