mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## What kind of change does this PR introduce? Refactor. Follow-up to #49871. ## What is the current behavior? Branded (green) text still uses the `text-brand` classname while the colour comes from `--primary`. ## What is the new behavior? **Rename-only:** `text-brand` → `text-primary` across callsites and docs. Leftover `bg-brand` / `border-brand` alias to `brand-default`. No intentional colour changes in this PR. This better matches how we treat our green in other components and props, like `variant="primary"` for green buttons. ## To test On light mode: smoke-test that branded text still looks like #49871 (readable green, not the bright fill). - [Homepage](https://zone-www-dot-com-git-dnywh-depr-316-text-brand-de2380-supabase.vercel.app/): “Scale to millions” uses `text-primary` - [Docs homepage](https://docs-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/docs): branded links like “More on self-hosting” are still readable - [Typography docs](https://design-system-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/design-system/docs/typography): documents `text-primary` - [Colour usage](https://design-system-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/design-system/docs/color-usage): `text-primary` is visibly darker than `bg-brand-default` _on light mode_ - [Studio auth providers](https://studio-staging-git-dnywh-depr-316-text-brand-to-60fa6c-supabase.vercel.app/dashboard/project/_/auth/providers): enabled provider badge text readable; status dot stays bright green <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Updated branded text, link hover states, icons, badges, indicators, and highlighted content across the Design System, Docs, Studio, Learn, UI Library, and marketing experiences to use the primary theme color. * Updated syntax highlighting and table-of-contents styling for consistent primary-color presentation. * Refined brand color fallback behavior for bright fills and borders. * **Documentation** * Updated color-usage and typography guidance to recommend the primary text utility. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
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-primary 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>
|
|
)
|
|
}
|