mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
The page scan stops at the article wrapper, so nothing checked the top nav, sidebar nav, table-of-contents sidebar, breadcrumbs, or footer. A violation in any of those lands on every docs page. Adds a global-elements Playwright project that scans the document with the article excluded, across six pages, one per layout, at two viewports, plus one pass with the mobile menu open. It runs from a docs_components paths filter in the existing workflow, so a content-only pull request never triggers it. Rules that pass today are enforced, so they catch a newly introduced violation without failing on existing debt. Everything else annotates. Supersedes #48909, which predated e2e/shared.
198 lines
7.3 KiB
TypeScript
198 lines
7.3 KiB
TypeScript
import { getCustomContent } from '~/lib/custom-content/getCustomContent'
|
|
import { useIsLoggedIn, useIsUserLoading } from 'common'
|
|
import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion'
|
|
import { X } from 'lucide-react'
|
|
import Image from 'next/image'
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import { Dispatch, Fragment, SetStateAction, useEffect } from 'react'
|
|
import { useKey } from 'react-use'
|
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, cn } from 'ui'
|
|
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
|
|
|
|
import type { DropdownMenuItem } from '../Navigation.types'
|
|
import { MenuItem, useActiveMenuLabel } from './GlobalNavigationMenu'
|
|
import { GLOBAL_MENU_ITEMS } from './NavigationMenu.constants'
|
|
|
|
const DEFAULT_EASE = [0.24, 0.25, 0.05, 1]
|
|
|
|
const container = {
|
|
hidden: { opacity: 0 },
|
|
show: { opacity: 1, transition: { duration: 0.15, staggerChildren: 0.05, ease: DEFAULT_EASE } },
|
|
exit: { opacity: 0, transition: { duration: 0.15 } },
|
|
}
|
|
|
|
const listItem = {
|
|
hidden: { opacity: 0, y: 10 },
|
|
show: { opacity: 1, y: 0, transition: { duration: 0.25, ease: DEFAULT_EASE } },
|
|
exit: { opacity: 0, transition: { duration: 0.05 } },
|
|
}
|
|
|
|
const itemClassName =
|
|
'block py-2 pl-2 pr-3.5 text-sm text-foreground-light hover:bg-surface-200 focus-ring rounded-sm'
|
|
|
|
const AccordionMenuItem = ({ section }: { section: DropdownMenuItem[] }) => {
|
|
const activeLabel = useActiveMenuLabel(GLOBAL_MENU_ITEMS)
|
|
|
|
return section[0].menuItems ? (
|
|
<AccordionItem
|
|
value={section[0].label}
|
|
className={cn(
|
|
'relative',
|
|
activeLabel === section[0].label && 'text-foreground!',
|
|
itemClassName
|
|
)}
|
|
>
|
|
<AccordionTrigger className="py-1">{section[0].label}</AccordionTrigger>
|
|
<AccordionContent>
|
|
{section[0].menuItems?.map((menuItem, menuItemIndex) => (
|
|
<Fragment key={`desktop-docs-menu-section-${menuItemIndex}`}>
|
|
{menuItem
|
|
.filter((item) => item.enabled !== false)
|
|
.map((item) =>
|
|
!item.href ? (
|
|
<div
|
|
key={item.label}
|
|
className="font-mono tracking-wider flex items-center text-foreground-muted text-xs uppercase rounded-md p-2 leading-none"
|
|
>
|
|
{item.label}
|
|
</div>
|
|
) : (
|
|
<MenuItem
|
|
key={item.label}
|
|
href={item.href}
|
|
title={item.label}
|
|
community={item.community}
|
|
new={item.new}
|
|
icon={item.icon}
|
|
/>
|
|
)
|
|
)}
|
|
</Fragment>
|
|
))}
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
) : (
|
|
<Link
|
|
href={section[0].href || '#'}
|
|
className={cn(activeLabel === section[0].label && 'text-foreground!', itemClassName)}
|
|
>
|
|
{section[0].label}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
const Menu = () => (
|
|
<Accordion type="multiple" className="space-y-1 mt-2.5">
|
|
{GLOBAL_MENU_ITEMS.filter((section) => section[0].enabled !== false).map((section, index) => (
|
|
<AccordionMenuItem key={index} section={section} />
|
|
))}
|
|
</Accordion>
|
|
)
|
|
interface Props {
|
|
open: boolean
|
|
setOpen: Dispatch<SetStateAction<boolean>>
|
|
}
|
|
|
|
const GlobalMobileMenu = ({ open, setOpen }: Props) => {
|
|
const isLoggedIn = useIsLoggedIn()
|
|
const isUserLoading = useIsUserLoading()
|
|
const pathname = usePathname()
|
|
|
|
const { navigationLogo } = getCustomContent(['navigation:logo'])
|
|
|
|
// Close mobile menu on route change
|
|
useEffect(() => {
|
|
setOpen(false)
|
|
}, [pathname])
|
|
|
|
useKey('Escape', () => setOpen(false))
|
|
|
|
return (
|
|
<LazyMotion features={domAnimation}>
|
|
<AnimatePresence mode="wait">
|
|
{open && (
|
|
<m.div
|
|
data-testid="sb-docs-mobile-menu"
|
|
variants={container}
|
|
initial="hidden"
|
|
animate="show"
|
|
exit="exit"
|
|
className="bg-overlay fixed overflow-hidden inset-0 z-50 h-screen max-h-screen w-screen supports-[height:100cqh]:h-[100cqh] supports-[height:100svh]:h-svh transform"
|
|
>
|
|
<div className="absolute px-5 h-(--header-height) flex items-center justify-between w-screen left-0 top-0 z-50 bg-overlay before:content[''] before:absolute before:w-full before:h-3 before:inset-0 before:top-full before:bg-linear-to-b before:from-background-overlay before:to-transparent">
|
|
<Link href="/" className="flex items-center gap-2">
|
|
<Image
|
|
className="cursor-pointer hidden dark:block"
|
|
src={navigationLogo?.dark ?? '/docs/supabase-dark.svg'}
|
|
priority
|
|
width={96}
|
|
height={24}
|
|
alt="Supabase Logo"
|
|
/>
|
|
<Image
|
|
className="cursor-pointer block dark:hidden"
|
|
src={navigationLogo?.light ?? '/docs/supabase-light.svg'}
|
|
priority
|
|
width={96}
|
|
height={24}
|
|
alt="Supabase Logo"
|
|
/>
|
|
<span className="font-mono text-sm font-medium text-brand-link mb-px">DOCS</span>
|
|
</Link>
|
|
<div className="flex gap-4 items-center">
|
|
<ThemeToggle contentClassName="bg-surface-200" />
|
|
<button
|
|
tabIndex={0}
|
|
onClick={() => setOpen(false)}
|
|
type="button"
|
|
className="inline-flex items-center justify-center bg-surface-100 hover:bg-surface-200 border border-default bg-surface-100/75 text-foreground-light rounded-sm min-w-[30px] w-[30px] h-[30px] focus-ring"
|
|
>
|
|
<span className="sr-only">Close menu</span>
|
|
<X />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="max-h-screen supports-[height:100cqh]:h-[100cqh] supports-[height:100svh]:h-svh overflow-y-auto pt-12 pb-32 px-3">
|
|
<Menu />
|
|
</div>
|
|
<div className="absolute bottom-0 left-0 right-0 top-auto w-full bg-alternative flex items-stretch p-4 gap-4">
|
|
{!isUserLoading && (
|
|
<>
|
|
{isLoggedIn ? (
|
|
<Button block size="medium" asChild>
|
|
<Link href="https://supabase.com/dashboard/projects">Dashboard</Link>
|
|
</Button>
|
|
) : (
|
|
<>
|
|
<Button block size="medium" variant="default" asChild>
|
|
<Link href="https://supabase.com/dashboard/sign-in">Sign in</Link>
|
|
</Button>
|
|
<Button block size="medium" asChild>
|
|
<Link href="https://supabase.com/dashboard/new">Start your project</Link>
|
|
</Button>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</m.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<AnimatePresence mode="wait">
|
|
{open && (
|
|
<m.div
|
|
variants={container}
|
|
initial="hidden"
|
|
animate="show"
|
|
exit="exit"
|
|
className="bg-alternative fixed overflow-hidden inset-0 z-40 h-screen w-screen transform"
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</LazyMotion>
|
|
)
|
|
}
|
|
|
|
export default GlobalMobileMenu
|