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>
273 lines
11 KiB
TypeScript
273 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { useIsLoggedIn, useIsUserLoading, useUser } from 'common'
|
|
import ScrollProgress from 'components/ScrollProgress'
|
|
import { getMenu } from 'data/nav'
|
|
import { DevToolbarTrigger } from 'dev-tools'
|
|
import { useSendTelemetryEvent } from 'lib/telemetry'
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import React, { useState } from 'react'
|
|
import { useWindowSize } from 'react-use'
|
|
import {
|
|
Button,
|
|
buttonVariants,
|
|
cn,
|
|
NavigationMenu,
|
|
NavigationMenuContent,
|
|
NavigationMenuItem,
|
|
NavigationMenuLink,
|
|
NavigationMenuList,
|
|
NavigationMenuTrigger,
|
|
NavigationMenuViewport,
|
|
} from 'ui'
|
|
import { AuthenticatedDropdownMenu } from 'ui-patterns/AuthenticatedDropdownMenu'
|
|
import { AnnouncementBanner } from 'ui-patterns/Banners/AnnouncementBanner'
|
|
|
|
import GitHubButton from './GitHubButton'
|
|
import HamburgerButton from './HamburgerMenu'
|
|
import MenuItem from './MenuItem'
|
|
import { MobileMenu } from './MobileMenu'
|
|
import RightClickBrandLogo from './RightClickBrandLogo'
|
|
import useDropdownMenu from './useDropdownMenu'
|
|
|
|
interface Props {
|
|
hideNavbar: boolean
|
|
stickyNavbar?: boolean
|
|
}
|
|
|
|
const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
|
|
const pathname = usePathname()
|
|
const { width } = useWindowSize()
|
|
const [open, setOpen] = useState(false)
|
|
const [activeDropdown, setActiveDropdown] = useState('')
|
|
const [visibleDropdown, setVisibleDropdown] = useState('')
|
|
const [isSwitchingDropdown, setIsSwitchingDropdown] = useState(false)
|
|
const [openedDropdowns, setOpenedDropdowns] = useState<string[]>([])
|
|
const handleDropdownChange = (value: string) => {
|
|
// animate card height between two open menus only
|
|
setIsSwitchingDropdown(value !== '' && activeDropdown !== '')
|
|
setActiveDropdown(value)
|
|
if (value === '') return
|
|
setVisibleDropdown(value)
|
|
if (!openedDropdowns.includes(value)) setOpenedDropdowns([...openedDropdowns, value])
|
|
}
|
|
const isLoggedIn = useIsLoggedIn()
|
|
const isUserLoading = useIsUserLoading()
|
|
const user = useUser()
|
|
const menu = getMenu()
|
|
const dropdownTitles: string[] = menu.primaryNav
|
|
.filter((menuItem) => menuItem.hasDropdown)
|
|
.map((menuItem) => menuItem.title)
|
|
const getDropdownSide = (title: string) => {
|
|
if (title === visibleDropdown) return 'active'
|
|
return dropdownTitles.indexOf(title) < dropdownTitles.indexOf(visibleDropdown) ? 'start' : 'end'
|
|
}
|
|
const sendTelemetryEvent = useSendTelemetryEvent()
|
|
const userMenu = useDropdownMenu(user)
|
|
|
|
const isGAWeekSection = pathname?.startsWith('/ga-week')
|
|
const isStateOfStartupsPage = pathname?.startsWith('/state-of-startups')
|
|
const disableStickyNav = isGAWeekSection || !stickyNavbar
|
|
const showLaunchWeekNavMode = isGAWeekSection && !open
|
|
|
|
const [scrolled, setScrolled] = React.useState(false)
|
|
React.useEffect(() => {
|
|
if (!isStateOfStartupsPage) return
|
|
const handleScroll = () => setScrolled(window.scrollY > 50)
|
|
window.addEventListener('scroll', handleScroll)
|
|
return () => window.removeEventListener('scroll', handleScroll)
|
|
}, [isStateOfStartupsPage])
|
|
|
|
const isTransparent = isStateOfStartupsPage && !scrolled && !open
|
|
|
|
React.useEffect(() => {
|
|
if (open) {
|
|
// Prevent scrolling on mount
|
|
document.body.style.overflow = 'hidden'
|
|
} else {
|
|
document.body.style.overflow = 'auto'
|
|
}
|
|
}, [open])
|
|
|
|
// Close mobile menu when desktop
|
|
React.useEffect(() => {
|
|
if (width >= 1024) setOpen(false)
|
|
}, [width])
|
|
|
|
if (hideNavbar) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{!isStateOfStartupsPage && <AnnouncementBanner />}
|
|
<div
|
|
className={cn(
|
|
'sticky top-0 z-40 transform',
|
|
disableStickyNav && 'relative',
|
|
isStateOfStartupsPage && 'fixed left-0 right-0'
|
|
)}
|
|
style={{ transform: 'translate3d(0,0,999px)' }}
|
|
data-nav-transparent={isTransparent ? '' : undefined}
|
|
>
|
|
{isStateOfStartupsPage && <AnnouncementBanner />}
|
|
<div
|
|
className={cn(
|
|
'absolute inset-0 h-full w-full bg-background/90 dark:bg-background/95 transition-all duration-300',
|
|
!showLaunchWeekNavMode && !isTransparent && 'opacity-100!',
|
|
showLaunchWeekNavMode && 'bg-transparent! dark:bg-black!',
|
|
isGAWeekSection && 'dark:bg-alternative!',
|
|
isTransparent && 'bg-transparent! dark:bg-transparent! opacity-100!'
|
|
)}
|
|
/>
|
|
<nav
|
|
className={cn(
|
|
`relative z-40 border-default border-b backdrop-blur-xs transition-all duration-300`,
|
|
showLaunchWeekNavMode && 'border-muted border-b bg-transparent',
|
|
isTransparent && 'border-transparent backdrop-blur-none'
|
|
)}
|
|
>
|
|
<div className="section-container relative flex justify-between h-16">
|
|
<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 />
|
|
</div>
|
|
<NavigationMenu
|
|
delayDuration={0}
|
|
value={activeDropdown}
|
|
onValueChange={handleDropdownChange}
|
|
renderViewport={false}
|
|
className="static hidden pl-8 lg:flex h-16 items-stretch"
|
|
>
|
|
<NavigationMenuList className="h-full space-x-0 items-stretch">
|
|
{menu.primaryNav.map((menuItem) =>
|
|
menuItem.hasDropdown ? (
|
|
<NavigationMenuItem
|
|
className="text-sm font-medium"
|
|
key={menuItem.title}
|
|
value={menuItem.title}
|
|
>
|
|
<NavigationMenuTrigger
|
|
className={cn(
|
|
buttonVariants({ variant: 'text', size: 'small' }),
|
|
'bg-transparent! hover:text-brand data-open:text-brand! focus-ring focus-visible:text-foreground px-2.5 h-full'
|
|
)}
|
|
>
|
|
{menuItem.title}
|
|
</NavigationMenuTrigger>
|
|
<NavigationMenuContent
|
|
forceMount
|
|
inert={visibleDropdown !== menuItem.title}
|
|
data-active={visibleDropdown === menuItem.title}
|
|
data-side={getDropdownSide(menuItem.title)}
|
|
className={cn(
|
|
'md:w-full data-[motion^=from-]:animate-none! data-[motion^=to-]:animate-none!',
|
|
'data-[active=false]:pointer-events-none data-[active=false]:opacity-0',
|
|
'data-[side=start]:-translate-x-4 data-[side=end]:translate-x-4',
|
|
'motion-safe:group-data-[switching=true]/viewport:transition-[opacity,translate]',
|
|
'motion-safe:group-data-[switching=true]/viewport:duration-250',
|
|
'motion-safe:group-data-[switching=true]/viewport:ease-in-out'
|
|
)}
|
|
>
|
|
{openedDropdowns.includes(menuItem.title) ? menuItem.dropdown : null}
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
) : (
|
|
<NavigationMenuItem className="text-sm font-medium" key={menuItem.title}>
|
|
<NavigationMenuLink asChild>
|
|
<MenuItem
|
|
href={menuItem.url}
|
|
title={menuItem.title}
|
|
className="group-hover:bg-transparent text-foreground focus-visible:text-brand px-2.5 h-full"
|
|
hoverColor="brand"
|
|
/>
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
)
|
|
)}
|
|
</NavigationMenuList>
|
|
<NavigationMenuViewport
|
|
forceMount
|
|
data-open={activeDropdown !== ''}
|
|
data-switching={isSwitchingDropdown}
|
|
containerProps={{ className: 'inset-x-0' }}
|
|
className={cn(
|
|
'group/viewport origin-top scale-100 rounded-xl bg-surface-75 md:w-[960px]',
|
|
'data-[state=open]:animate-none! data-[state=closed]:animate-none!',
|
|
'data-[state=open]:duration-200 data-[state=open]:ease-out data-[state=closed]:duration-200',
|
|
'data-[open=false]:invisible data-[open=false]:scale-[0.97] data-[open=false]:opacity-0',
|
|
'data-[open=false]:pointer-events-none',
|
|
'motion-safe:transition-[opacity,scale,visibility]',
|
|
'motion-safe:data-[switching=true]:transition-[height,opacity,scale,visibility]',
|
|
'motion-reduce:transition-none'
|
|
)}
|
|
/>
|
|
</NavigationMenu>
|
|
</div>
|
|
<div className="flex items-center gap-2 opacity-0 animate-fade-in scale-100! delay-300">
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-2 transition-opacity',
|
|
isUserLoading ? 'opacity-0' : 'opacity-100'
|
|
)}
|
|
>
|
|
<DevToolbarTrigger />
|
|
<GitHubButton />
|
|
{isLoggedIn ? (
|
|
<>
|
|
<Button className="hidden lg:block" asChild>
|
|
<Link href="/dashboard/projects">Dashboard</Link>
|
|
</Button>
|
|
<AuthenticatedDropdownMenu menu={userMenu} user={user} site="www" />
|
|
</>
|
|
) : (
|
|
<>
|
|
<Button className="hidden lg:block" asChild>
|
|
<Link
|
|
href="https://supabase.com/dashboard"
|
|
onClick={() =>
|
|
sendTelemetryEvent({
|
|
action: 'sign_in_button_clicked',
|
|
properties: { buttonLocation: 'Header Nav' },
|
|
})
|
|
}
|
|
>
|
|
Sign in
|
|
</Link>
|
|
</Button>
|
|
<Button variant="primary" className="hidden lg:block" asChild>
|
|
<Link
|
|
href="https://supabase.com/dashboard/sign-up"
|
|
onClick={() =>
|
|
sendTelemetryEvent({
|
|
action: 'start_project_button_clicked',
|
|
properties: { buttonLocation: 'Header Nav' },
|
|
})
|
|
}
|
|
>
|
|
Start your project
|
|
</Link>
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<HamburgerButton
|
|
toggleFlyOut={() => setOpen(true)}
|
|
showLaunchWeekNavMode={showLaunchWeekNavMode}
|
|
/>
|
|
</div>
|
|
<MobileMenu open={open} setOpen={setOpen} menu={menu} />
|
|
</nav>
|
|
|
|
<ScrollProgress />
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default Nav
|