mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 13:19:09 +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.
441 lines
11 KiB
TypeScript
441 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import type { NavMenuSection } from '~/components/Navigation/Navigation.types'
|
|
import DefaultNavigationMenu, {
|
|
type MenuId,
|
|
} from '~/components/Navigation/NavigationMenu/NavigationMenu'
|
|
import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils'
|
|
import TopNavBar from '~/components/Navigation/NavigationMenu/TopNavBar'
|
|
import { DOCS_CONTENT_CONTAINER_ID } from '~/features/ui/helpers.constants'
|
|
import { menuState, useMenuMobileOpen } from '~/hooks/useMenuState'
|
|
// End of third-party imports
|
|
|
|
import { isFeatureEnabled } from 'common'
|
|
import dynamic from 'next/dynamic'
|
|
import { usePathname } from 'next/navigation'
|
|
import { memo, useEffect, type PropsWithChildren, type ReactNode } from 'react'
|
|
import { cn } from 'ui'
|
|
|
|
const Footer = dynamic(() => import('~/components/Navigation/Footer'))
|
|
|
|
const footerEnabled = isFeatureEnabled('docs:footer')
|
|
|
|
const levelsData = {
|
|
home: {
|
|
icon: 'home',
|
|
name: 'Home',
|
|
},
|
|
gettingstarted: {
|
|
icon: 'getting-started',
|
|
name: 'Getting Started',
|
|
},
|
|
database: {
|
|
icon: 'database',
|
|
name: 'Database',
|
|
},
|
|
cron: {
|
|
icon: 'cron',
|
|
name: 'Cron',
|
|
},
|
|
queues: {
|
|
icon: 'queues',
|
|
name: 'Queues',
|
|
},
|
|
api: {
|
|
icon: 'rest',
|
|
name: 'REST API',
|
|
},
|
|
graphql: {
|
|
icon: 'graphql',
|
|
name: 'GraphQL',
|
|
},
|
|
auth: {
|
|
icon: 'auth',
|
|
name: 'Auth',
|
|
},
|
|
functions: {
|
|
icon: 'edge-functions',
|
|
name: 'Edge Functions',
|
|
},
|
|
telemetry: {
|
|
icon: 'telemetry',
|
|
name: 'Telemetry',
|
|
},
|
|
realtime: {
|
|
icon: 'realtime',
|
|
name: 'Realtime',
|
|
},
|
|
analytics: {
|
|
icon: 'analytics',
|
|
name: 'Analytics',
|
|
},
|
|
storage: {
|
|
icon: 'storage',
|
|
name: 'Storage',
|
|
},
|
|
ai: {
|
|
icon: 'ai',
|
|
name: 'AI & Vectors',
|
|
},
|
|
local_development: {
|
|
icon: 'reference-cli',
|
|
name: 'Local Development',
|
|
},
|
|
security: {
|
|
icon: 'platform',
|
|
name: 'Security',
|
|
},
|
|
platform: {
|
|
icon: 'platform',
|
|
name: 'Platform',
|
|
},
|
|
contributing: {
|
|
icon: 'contributing',
|
|
name: 'Contributing',
|
|
},
|
|
resources: {
|
|
icon: 'resources',
|
|
name: 'Resources',
|
|
},
|
|
self_hosting: {
|
|
icon: 'self-hosting',
|
|
name: 'Self-Hosting',
|
|
},
|
|
integrations: {
|
|
icon: 'integrations',
|
|
name: 'Integrations',
|
|
},
|
|
reference_server_v1: {
|
|
icon: 'reference-javascript',
|
|
name: 'Server Reference v1.0',
|
|
},
|
|
reference_javascript_v1: {
|
|
icon: 'reference-javascript',
|
|
name: 'JavaScript Reference v1.0',
|
|
},
|
|
reference_javascript_v2: {
|
|
icon: 'reference-javascript',
|
|
name: 'JavaScript Reference v2.0',
|
|
},
|
|
reference_dart_v1: {
|
|
icon: 'reference-dart',
|
|
name: 'Dart Reference v1.0',
|
|
},
|
|
reference_dart_v2: {
|
|
icon: 'reference-dart',
|
|
name: 'Dart Reference v2.0',
|
|
},
|
|
reference_csharp_v0: {
|
|
icon: 'reference-csharp',
|
|
name: 'C# Reference v0.0',
|
|
},
|
|
reference_csharp_v1: {
|
|
icon: 'reference-csharp',
|
|
name: 'C# Reference v1.0',
|
|
},
|
|
reference_python_v2: {
|
|
icon: 'reference-python',
|
|
name: 'Python Reference v2.0',
|
|
},
|
|
reference_swift_v1: {
|
|
icon: 'reference-swift',
|
|
name: 'Swift Reference v1.0',
|
|
},
|
|
reference_swift_v2: {
|
|
icon: 'reference-swift',
|
|
name: 'Swift Reference v2.0',
|
|
},
|
|
reference_kotlin_v1: {
|
|
icon: 'reference-kotlin',
|
|
name: 'Kotlin Reference v1.0',
|
|
},
|
|
reference_kotlin_v2: {
|
|
icon: 'reference-kotlin',
|
|
name: 'Kotlin Reference v2.0',
|
|
},
|
|
reference_kotlin_v3: {
|
|
icon: 'reference-kotlin',
|
|
name: 'Kotlin Reference v3.0',
|
|
},
|
|
reference_cli: {
|
|
icon: 'reference-cli',
|
|
name: 'CLI Reference',
|
|
},
|
|
reference_api: {
|
|
icon: 'reference-api',
|
|
name: 'Management API Reference',
|
|
},
|
|
reference_self_hosting_auth: {
|
|
icon: 'reference-auth',
|
|
name: 'Auth Server Reference',
|
|
},
|
|
reference_self_hosting_storage: {
|
|
icon: 'reference-storage',
|
|
name: 'Storage Server Reference',
|
|
},
|
|
reference_self_hosting_realtime: {
|
|
icon: 'reference-realtime',
|
|
name: 'Realtime Server Reference',
|
|
},
|
|
reference_self_hosting_analytics: {
|
|
icon: 'reference-analytics',
|
|
name: 'Analytics Server Reference',
|
|
},
|
|
reference_self_hosting_functions: {
|
|
icon: 'reference-functions',
|
|
name: 'Functions Server Reference',
|
|
},
|
|
}
|
|
|
|
type MobileHeaderProps = { menuId: MenuId } | { menuName: string }
|
|
|
|
const MobileHeader = memo(function MobileHeader(props: MobileHeaderProps) {
|
|
const mobileMenuOpen = useMenuMobileOpen()
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'lg:hidden px-3.5 border-b z-10',
|
|
'transition-all ease-out',
|
|
'top-0',
|
|
mobileMenuOpen && 'absolute',
|
|
'flex items-center',
|
|
mobileMenuOpen ? 'gap-0' : 'gap-1'
|
|
)}
|
|
>
|
|
<button
|
|
tabIndex={0}
|
|
className={cn(
|
|
'h-8 w-8 flex group items-center justify-center mr-1',
|
|
mobileMenuOpen && 'mt-0.5'
|
|
)}
|
|
onClick={() => menuState.setMenuMobileOpen(!mobileMenuOpen)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'space-y-1 cursor-pointer relative',
|
|
mobileMenuOpen ? 'w-4 h-4' : 'w-4 h-[8px]'
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
'transition-all ease-out block w-4 h-px bg-foreground-muted group-hover:bg-foreground',
|
|
!mobileMenuOpen ? 'w-4' : 'absolute rotate-45 top-[6px]'
|
|
)}
|
|
/>
|
|
<span
|
|
className={cn(
|
|
'transition-all ease-out block h-px bg-foreground-muted group-hover:bg-foreground',
|
|
!mobileMenuOpen ? 'w-3 group-hover:w-4' : 'absolute w-4 -rotate-45 top-[2px]'
|
|
)}
|
|
/>
|
|
</div>
|
|
</button>
|
|
<span
|
|
className={cn(
|
|
'transition-all duration-200',
|
|
'text-foreground',
|
|
mobileMenuOpen ? 'text-xs' : 'text-sm'
|
|
)}
|
|
>
|
|
{mobileMenuOpen
|
|
? 'Close'
|
|
: 'menuId' in props
|
|
? (levelsData[props.menuId]?.name ?? levelsData['home'].name)
|
|
: props.menuName}
|
|
</span>
|
|
</div>
|
|
)
|
|
})
|
|
|
|
const MobileMenuBackdrop = memo(function MobileMenuBackdrop() {
|
|
const mobileMenuOpen = useMenuMobileOpen()
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('resize', (e: UIEvent) => {
|
|
const w = e.target as Window
|
|
if (mobileMenuOpen && w.innerWidth >= 1024) {
|
|
menuState.setMenuMobileOpen(!mobileMenuOpen)
|
|
}
|
|
})
|
|
return () => {
|
|
window.removeEventListener('resize', () => {})
|
|
}
|
|
}, [mobileMenuOpen])
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'h-full',
|
|
'left-0',
|
|
'right-0',
|
|
'z-10',
|
|
'backdrop-blur-xs backdrop-filter bg-alternative/90',
|
|
mobileMenuOpen ? 'absolute h-full w-full top-0 left-0' : 'hidden h-0',
|
|
// always hide on desktop
|
|
'lg:hidden'
|
|
)}
|
|
onClick={() => menuState.setMenuMobileOpen(!mobileMenuOpen)}
|
|
></div>
|
|
)
|
|
})
|
|
|
|
const Container = memo(function Container({
|
|
children,
|
|
className,
|
|
}: PropsWithChildren<{ className?: string }>) {
|
|
return (
|
|
<main
|
|
// used by layout to scroll to top
|
|
id={DOCS_CONTENT_CONTAINER_ID}
|
|
tabIndex={-1}
|
|
className={cn(
|
|
'w-full transition-all ease-out relative scroll-mt-(--header-height) outline-hidden',
|
|
// desktop override any margin styles
|
|
'lg:ml-0',
|
|
className
|
|
)}
|
|
>
|
|
<div className="flex flex-col sticky top-0">{children}</div>
|
|
</main>
|
|
)
|
|
})
|
|
|
|
const NavContainer = memo(function NavContainer({ children }: PropsWithChildren) {
|
|
const mobileMenuOpen = useMenuMobileOpen()
|
|
|
|
return (
|
|
<nav
|
|
aria-labelledby="main-nav-title"
|
|
data-testid="sb-docs-sidebar-nav"
|
|
className={cn(
|
|
'fixed lg:relative z-40 lg:z-auto',
|
|
mobileMenuOpen ? 'w-[75%] sm:w-[50%] md:w-[33%] left-0' : 'w-0 -left-full',
|
|
'lg:w-[420px] !lg:left-0',
|
|
'lg:top-(--header-height) lg:sticky',
|
|
'h-screen lg:h-[calc(100vh-var(--header-height))]',
|
|
// desktop override any left styles
|
|
'lg:left-0',
|
|
'transition-all',
|
|
'top-0 bottom-0',
|
|
'flex flex-col ml-0',
|
|
'border-r',
|
|
'lg:overflow-y-auto'
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'top-0 lg:top-(--header-height)',
|
|
'h-full',
|
|
'relative lg:sticky',
|
|
'w-full lg:w-auto',
|
|
'h-fit lg:h-screen overflow-y-scroll lg:overflow-auto',
|
|
'overscroll-contain',
|
|
'backdrop-blur-sm backdrop-filter bg-background',
|
|
'flex flex-col grow'
|
|
)}
|
|
>
|
|
<span id="main-nav-title" className="sr-only">
|
|
Main menu
|
|
</span>
|
|
<div className="top-0 sticky h-0 z-10">
|
|
<div className="bg-linear-to-b from-background to-transparent h-4 w-full"></div>
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
'transition-all ease-out duration-200',
|
|
'absolute left-0 right-0',
|
|
'px-5 pl-5 pt-6 pb-16 lg:pb-32',
|
|
'bg-background',
|
|
// desktop styles
|
|
'lg:relative lg:left-0 lg:pb-10 lg:px-10 lg:flex',
|
|
'lg:opacity-100 lg:visible'
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
)
|
|
})
|
|
|
|
interface SkeletonProps extends PropsWithChildren {
|
|
menuId?: MenuId
|
|
menuName?: string
|
|
hideSideNav?: boolean
|
|
NavigationMenu?: ReactNode
|
|
hideFooter?: boolean
|
|
className?: string
|
|
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
|
|
}
|
|
|
|
function TopNavSkeleton({ children }) {
|
|
return (
|
|
<div className="flex flex-col h-full w-full">
|
|
<div className="hidden lg:sticky w-full lg:flex top-0 left-0 right-0 z-50">
|
|
<TopNavBar />
|
|
</div>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SidebarSkeleton({
|
|
children,
|
|
menuId: _menuId,
|
|
menuName,
|
|
NavigationMenu,
|
|
hideFooter = !footerEnabled,
|
|
className,
|
|
hideSideNav,
|
|
additionalNavItems,
|
|
}: SkeletonProps) {
|
|
const pathname = usePathname()
|
|
const menuId = _menuId ?? getMenuId(pathname)
|
|
|
|
const mobileMenuOpen = useMenuMobileOpen()
|
|
|
|
return (
|
|
<div className={cn('flex flex-row h-full relative', className)}>
|
|
{!hideSideNav && (
|
|
<NavContainer>
|
|
{NavigationMenu ?? (
|
|
<DefaultNavigationMenu menuId={menuId} additionalNavItems={additionalNavItems} />
|
|
)}
|
|
</NavContainer>
|
|
)}
|
|
<Container>
|
|
<div
|
|
className={cn(
|
|
'flex lg:hidden w-full top-0 left-0 right-0 z-50',
|
|
hideSideNav && 'sticky',
|
|
mobileMenuOpen && 'z-10'
|
|
)}
|
|
>
|
|
<TopNavBar />
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
'sticky',
|
|
'transition-all top-0 z-10',
|
|
'backdrop-blur-sm backdrop-filter bg-background'
|
|
)}
|
|
>
|
|
{hideSideNav ? null : menuName ? (
|
|
<MobileHeader menuName={menuName} />
|
|
) : (
|
|
<MobileHeader menuId={menuId} />
|
|
)}
|
|
</div>
|
|
<div className="grow">
|
|
{children}
|
|
{!hideFooter && <Footer />}
|
|
</div>
|
|
<MobileMenuBackdrop />
|
|
</Container>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { SidebarSkeleton, TopNavSkeleton }
|