mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
a31ca2bad0
## What kind of change does this PR introduce? Bug fix and design-system update. Resolves DEPR-316. Follow-up rename (`text-brand` → `text-primary`) is in a dedicated PR (https://github.com/supabase/supabase/pull/50564) stacked on this one. ## What is the current behavior? `text-brand` resolves to the canonical bright brand green in places that need readable text, which fails WCAG AA on light surfaces. A separate `text-brand-display` token adds another green for large type. ## What is the new behavior? - `text-brand` maps to accessible `--primary` (light mode darkened to meet ~4.5:1 AA) - `--hue` / `--primary-hue` aligned to 157.5 - `text-brand-display` removed; former display callsites use `text-brand` - Bright fills/borders stay on `brand-default` - Design-system colour and typography docs updated | Before | After | | --- | --- | | <img width="514" height="112" alt="CleanShot 2026-09-02 at 11 13 09@2x" src="https://github.com/user-attachments/assets/4e0138a9-a32d-4e4c-a426-90736706e1e7" /> | <img width="512" height="138" alt="CleanShot 2026-09-21 at 11 42 05@2x" src="https://github.com/user-attachments/assets/164cc5b1-a0c5-4e93-95f1-80016641a114" /> | | <img width="864" height="266" alt="CleanShot 2026-09-02 at 11 13 53@2x" src="https://github.com/user-attachments/assets/3c1ca53f-bf9e-431e-bc15-816b4a275b8e" /> | <img width="882" height="248" alt="CleanShot 2026-09-21 at 11 41 37@2x" src="https://github.com/user-attachments/assets/24828e7b-ed6b-44cb-b9dc-becc3398bdfc" /> | | <img width="782" height="692" alt="CleanShot 2026-09-02 at 11 16 30@2x" src="https://github.com/user-attachments/assets/fc871977-77bc-47fb-9e0e-9284e0ecd5cc" /> | <img width="730" height="690" alt="CleanShot 2026-09-21 at 11 42 52@2x" src="https://github.com/user-attachments/assets/bf479515-d5f9-471e-b82d-f097c0f4b56c" /> | | <img width="480" height="306" alt="CleanShot 2026-09-02 at 11 18 53@2x" src="https://github.com/user-attachments/assets/03f341f4-f02e-44f8-a2b2-8c31670d0427" /> | <img width="470" height="300" alt="CleanShot 2026-09-21 at 11 43 19@2x" src="https://github.com/user-attachments/assets/9df18217-d5e6-48b8-ba0b-579d2664b94b" /> | | <img width="960" height="300" alt="CleanShot 2026-09-02 at 11 32 04@2x" src="https://github.com/user-attachments/assets/6b1d9373-7a71-4247-81ff-26441604b09d" /> | <img width="980" height="306" alt="CleanShot 2026-09-21 at 11 44 13@2x" src="https://github.com/user-attachments/assets/41ad4784-02ec-4b29-b860-32af9fa79aa8" /> | | <img width="924" height="214" alt="CleanShot 2026-09-02 at 11 34 44@2x" src="https://github.com/user-attachments/assets/1de661fe-c7b6-499b-a94f-e4737436ec79" /> | <img width="752" height="162" alt="CleanShot 2026-09-21 at 11 44 56@2x" src="https://github.com/user-attachments/assets/1811890f-0660-4445-84e9-447720954fa1" /> | ## To test Test each callsite **in light mode** (dark mode is largely unchanged). ### WWW - [Homepage](https://zone-www-dot-com-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/): “Scale to millions” uses readable brand text (display token is gone) - [Careers](https://zone-www-dot-com-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/careers): small “Careers” eyebrow readable; green dividers stay bright `brand-default` - [Contact](https://zone-www-dot-com-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/contact-us): email / policy links use readable brand text - [Regions](https://zone-www-dot-com-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/regions): “Ask about early access to BYOC” readable ### Docs - [Docs homepage](https://docs-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs): “DOCS” wordmark and resource links readable - [Database overview](https://docs-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs/guides/database/overview): nav / footer brand links readable - [JavaScript reference](https://docs-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs/reference/javascript/introduction): active sidebar treatment readable ### Design system - [Typography](https://design-system-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs/typography): documents `text-brand` only (no display) - [Colour usage](https://design-system-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs/color-usage): `text-brand` vs `bg-brand-default` - [Design-system homepage](https://design-system-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/): brand text examples across themes ### Studio - [Auth providers](https://studio-staging-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/dashboard/project/_/auth/providers): enabled provider badge text readable; status dot stays bright - [Database policies](https://studio-staging-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/dashboard/project/_/database/policies?new=true): template hover text more legible - [Database connections](https://studio-staging-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/dashboard/project/_/observability/connections): “Live” status readable; animated dot stays bright green --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
246 lines
7.3 KiB
TypeScript
246 lines
7.3 KiB
TypeScript
'use client'
|
|
|
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { cn } from 'ui'
|
|
import { useBreakpoint } from 'common'
|
|
import { AnimatedGridBackground } from '../AnimatedGridBackground'
|
|
|
|
const stats = [
|
|
{ label: 'GitHub stars', value: '+10,000' },
|
|
{ label: 'Databases created', value: '15M+' },
|
|
{ label: 'Realtime messages', value: '280B+' },
|
|
{ label: 'Edge Functions invoked', value: '49B+' },
|
|
{ label: 'Petabytes stored', value: '64 PB' },
|
|
{ label: 'Peak connections', value: '193K' },
|
|
{ label: 'Projects created', value: '12M+' },
|
|
{ label: 'Images transformed', value: '64M+' },
|
|
]
|
|
|
|
// Animation timing configuration
|
|
const BUBBLE_CONFIG = {
|
|
// Lifetime
|
|
lifetimeMin: 5000, // minimum bubble lifetime in ms
|
|
lifetimeMax: 10000, // maximum bubble lifetime in ms
|
|
|
|
// Spawn timing
|
|
spawnIntervalMin: 2000, // minimum time between spawns in ms
|
|
spawnIntervalMax: 3500, // maximum time between spawns in ms
|
|
initialDelays: [1200, 1300, 1100, 1400], // staggered initial spawn delays (3-4 bubbles)
|
|
maxBubbles: 4, // maximum bubbles visible at once
|
|
|
|
// Positioning
|
|
minDistance: 18, // minimum distance between bubble centers (percentage)
|
|
}
|
|
|
|
type FloatingStatBubble = {
|
|
id: number
|
|
stat: (typeof stats)[number]
|
|
top: number
|
|
left: number
|
|
}
|
|
|
|
function getDistance(
|
|
box1: { top: number; left: number },
|
|
box2: { top: number; left: number }
|
|
): number {
|
|
return Math.sqrt(Math.pow(box1.top - box2.top, 2) + Math.pow(box1.left - box2.left, 2))
|
|
}
|
|
|
|
function generateNonOverlappingPosition(
|
|
existingBubbles: FloatingStatBubble[],
|
|
isMobile: boolean
|
|
): { top: number; left: number } | null {
|
|
const maxAttempts = 20
|
|
|
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
const top = isMobile
|
|
? Math.random() * 50 + 10 // 10-60% on mobile
|
|
: Math.random() * 70 + 10 // 10-80% on desktop
|
|
// On mobile, use a narrower, more centered range to prevent overflow
|
|
const left = isMobile
|
|
? Math.random() * 20 + 20 // 20-40% on mobile
|
|
: Math.random() * 70 + 10 // 10-80% on desktop
|
|
|
|
const hasOverlap = existingBubbles.some(
|
|
(bubble) =>
|
|
getDistance({ top, left }, { top: bubble.top, left: bubble.left }) <
|
|
BUBBLE_CONFIG.minDistance
|
|
)
|
|
|
|
if (!hasOverlap) {
|
|
return { top, left }
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function generateRandomBubble(
|
|
id: number,
|
|
existingBubbles: FloatingStatBubble[],
|
|
usedStatIndices: Set<number>,
|
|
isMobile: boolean
|
|
): FloatingStatBubble | null {
|
|
const position = generateNonOverlappingPosition(existingBubbles, isMobile)
|
|
|
|
if (!position) {
|
|
return null
|
|
}
|
|
|
|
// Find an unused stat, or pick a random one if all have been used
|
|
let statIndex: number
|
|
const availableIndices = stats.map((_, i) => i).filter((i) => !usedStatIndices.has(i))
|
|
|
|
if (availableIndices.length > 0) {
|
|
statIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)]
|
|
} else {
|
|
statIndex = Math.floor(Math.random() * stats.length)
|
|
}
|
|
|
|
return {
|
|
id,
|
|
stat: stats[statIndex],
|
|
top: position.top,
|
|
left: position.left,
|
|
}
|
|
}
|
|
|
|
function FloatingStatBubbles() {
|
|
const [bubbles, setBubbles] = useState<FloatingStatBubble[]>([])
|
|
const bubbleIdRef = useRef(0)
|
|
const usedStatIndicesRef = useRef<Set<number>>(new Set())
|
|
const lifetimeTimersRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set())
|
|
const isMobile = useBreakpoint(768)
|
|
|
|
useEffect(() => {
|
|
const spawnBubble = () => {
|
|
setBubbles((prev) => {
|
|
// Limit max bubbles on screen
|
|
if (prev.length >= BUBBLE_CONFIG.maxBubbles) {
|
|
return prev
|
|
}
|
|
|
|
const id = bubbleIdRef.current++
|
|
const newBubble = generateRandomBubble(id, prev, usedStatIndicesRef.current, isMobile)
|
|
|
|
if (!newBubble) {
|
|
return prev
|
|
}
|
|
|
|
// Track which stat index was used
|
|
const statIndex = stats.indexOf(newBubble.stat)
|
|
usedStatIndicesRef.current.add(statIndex)
|
|
|
|
const lifetime =
|
|
Math.random() * (BUBBLE_CONFIG.lifetimeMax - BUBBLE_CONFIG.lifetimeMin) +
|
|
BUBBLE_CONFIG.lifetimeMin
|
|
const timerId = setTimeout(() => {
|
|
setBubbles((current) => current.filter((bubble) => bubble.id !== id))
|
|
usedStatIndicesRef.current.delete(statIndex)
|
|
lifetimeTimersRef.current.delete(timerId)
|
|
}, lifetime)
|
|
lifetimeTimersRef.current.add(timerId)
|
|
|
|
return [...prev, newBubble]
|
|
})
|
|
}
|
|
|
|
const initialTimers = BUBBLE_CONFIG.initialDelays.map((delay) => setTimeout(spawnBubble, delay))
|
|
|
|
let intervalId: ReturnType<typeof setTimeout>
|
|
const scheduleNextSpawn = () => {
|
|
intervalId = setTimeout(
|
|
() => {
|
|
spawnBubble()
|
|
scheduleNextSpawn()
|
|
},
|
|
BUBBLE_CONFIG.spawnIntervalMin +
|
|
Math.random() * (BUBBLE_CONFIG.spawnIntervalMax - BUBBLE_CONFIG.spawnIntervalMin)
|
|
)
|
|
}
|
|
scheduleNextSpawn()
|
|
|
|
return () => {
|
|
initialTimers.forEach(clearTimeout)
|
|
clearTimeout(intervalId)
|
|
lifetimeTimersRef.current.forEach(clearTimeout)
|
|
lifetimeTimersRef.current.clear()
|
|
}
|
|
}, [isMobile])
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{bubbles.map((bubble) => (
|
|
<motion.div
|
|
key={bubble.id}
|
|
className={cn(
|
|
'absolute pointer-events-none px-3 py-2 bg-surface-100 border rounded-lg shadow-xs'
|
|
)}
|
|
style={{
|
|
top: `${bubble.top}%`,
|
|
left: `${bubble.left}%`,
|
|
}}
|
|
initial={{ opacity: 0, scale: 0.98 }}
|
|
animate={{ opacity: 0.98, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.9 }}
|
|
transition={{
|
|
opacity: { duration: 0.75, type: 'spring' },
|
|
scale: {
|
|
type: 'spring',
|
|
duration: 0.7,
|
|
bounce: 0.15,
|
|
},
|
|
}}
|
|
>
|
|
<p className="text-sm font-mono font-medium text-brand whitespace-nowrap">
|
|
{bubble.stat.value}
|
|
</p>
|
|
<p className="text-xs text-foreground-light whitespace-nowrap">{bubble.stat.label}</p>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
export function Home() {
|
|
return (
|
|
<div className="h-[calc(100dvh-64px)] flex flex-col max-w-240 mx-auto w-[95%] md:w-full">
|
|
<section className="relative border-x border-b h-full">
|
|
{/* Grid background */}
|
|
<AnimatedGridBackground
|
|
cols={5}
|
|
rows={{
|
|
desktop: 4,
|
|
mobile: 8,
|
|
}}
|
|
tiles={[
|
|
{ cell: 1, type: 'dots' },
|
|
{ cell: 6, type: 'dots' },
|
|
{ cell: 7, type: 'stripes' },
|
|
{ cell: 8, type: 'stripes' },
|
|
{ cell: 13, type: 'dots' },
|
|
{ cell: 18, type: 'dots' },
|
|
]}
|
|
/>
|
|
|
|
{/* Floating stat bubbles */}
|
|
<FloatingStatBubbles />
|
|
|
|
{/* Content */}
|
|
<div className="flex flex-col justify-end h-full px-4 lg:px-8 py-0 relative">
|
|
<h1 className="font-medium tracking-tighter text-6xl md:text-7xl lg:text-[5.6rem] translate-y-2 lg:translate-y-[10px]">
|
|
Supabase Wrapped
|
|
</h1>
|
|
</div>
|
|
</section>
|
|
|
|
<div className="relative w-full border-x border-b px-6 lg:px-10 py-12">
|
|
<article className="text-lg">
|
|
<p>You created more Supabase databases in 2025 than in all previous years combined.</p>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|