feat(docs): move reference navigation into right rail toc

This commit is contained in:
anthony lio committed 2026-09-29 15:43:08 +03:00
1 parent 695b5443b0
commit e4e88d34ab
6 files changed
+151 -127

No files matched your search

+12 -11
View File
@@ -27,21 +27,22 @@ async function ApiOperationPage({ operationSlug }: { operationSlug: string }) {
const section = sectionsBySlug?.get(operationSlug)
if (!section) notFound()
const navigationProps = {
libraryId: 'api',
name: 'Management API',
menuData: reference_api,
libPath: 'api',
version: 'latest',
isLatestVersion: true,
realNavigation: true,
}
return (
<ReferenceContentScrollHandler libPath="api" version="latest" isLatestVersion={true}>
<SidebarSkeleton
menuId={MenuId.RefApi}
NavigationMenu={
<ReferenceNavigation
libraryId="api"
name="Management API"
menuData={reference_api}
libPath="api"
version="latest"
isLatestVersion={true}
realNavigation
/>
}
NavigationMenu={<ReferenceNavigation {...navigationProps} />}
rightRail={<ReferenceNavigation {...navigationProps} hasActiveCrumb />}
>
<LayoutMainContent>
<article className="@container/article">
+11 -10
View File
@@ -8,20 +8,21 @@ import { LayoutMainContent } from '~/layouts/DefaultLayout'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
export async function CliReferencePage() {
const navigationProps = {
libraryId: 'cli',
name: 'Supabase CLI',
menuData: reference_cli,
libPath: 'cli',
version: 'latest',
isLatestVersion: true,
}
return (
<ReferenceContentScrollHandler libPath="cli" version="latest" isLatestVersion={true}>
<SidebarSkeleton
menuId={MenuId.RefCli}
NavigationMenu={
<ReferenceNavigation
libraryId="cli"
name="Supabase CLI"
menuData={reference_cli}
libPath="cli"
version="latest"
isLatestVersion={true}
/>
}
NavigationMenu={<ReferenceNavigation {...navigationProps} />}
rightRail={<ReferenceNavigation {...navigationProps} hasActiveCrumb />}
>
<LayoutMainContent>
<article className="@container/article">
@@ -7,6 +7,7 @@ import {
} from '~/components/Navigation/NavSection'
import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { isElementInViewport } from '~/features/ui/helpers.dom'
import { HeadingSlotCrumb } from '~/layouts/HeadingSlot'
import { BASE_PATH } from '~/lib/constants'
import { debounce } from 'lodash-es'
import Link from 'next/link'
@@ -25,6 +26,19 @@ import {
} from 'react'
import { cn } from 'ui'
export interface ReferenceCrumbHeading {
url: string
title: string
}
interface ReferenceActiveCrumbProps {
basePath: string
headings: ReferenceCrumbHeading[]
}
export const ACTIVE_BAR_CLASS_NAME =
'aria-[current=page]:before:absolute aria-[current=page]:before:-left-[13px] aria-[current=page]:before:top-1/2 aria-[current=page]:before:h-[1em] aria-[current=page]:before:w-px aria-[current=page]:before:-translate-y-1/2 aria-[current=page]:before:bg-brand'
export const ReferenceContentInitiallyScrolledContext = createContext<boolean>(false)
let patchCount = 0
@@ -126,38 +140,27 @@ export function ReferenceContentScrollHandler({
export function ReferenceNavigationScrollHandler({
children,
...rest
}: PropsWithChildren & HTMLAttributes<HTMLDivElement>) {
const parentRef = useRef<HTMLElement | null>(null)
const ref = useRef<HTMLDivElement | null>(null)
}: PropsWithChildren & HTMLAttributes<HTMLElement>) {
const ref = useRef<HTMLElement | null>(null)
const initialScrollHappened = useContext(ReferenceContentInitiallyScrolledContext)
useEffect(() => {
if (!ref.current) return
const scrollActiveIntoView = useCallback(() => {
if (!ref.current?.offsetParent) return
const currentLink = ref.current.querySelector<HTMLElement>('[aria-current=page]')
if (!currentLink || isElementInViewport(currentLink)) return
let scrollingParent: HTMLElement = ref.current
while (scrollingParent && !(scrollingParent.scrollHeight > scrollingParent.clientHeight)) {
const parent = scrollingParent.parentElement
if (!parent) break
scrollingParent = parent
while (scrollingParent.scrollHeight <= scrollingParent.clientHeight) {
if (!scrollingParent.parentElement) return
scrollingParent = scrollingParent.parentElement
}
parentRef.current = scrollingParent
}, [])
const scrollActiveIntoView = useCallback(() => {
const currentLink = ref.current?.querySelector('[aria-current=page]') as HTMLElement
if (currentLink && !isElementInViewport(currentLink)) {
// Calculate the offset of the current link relative to scrollingParent
// and scroll the parent to the top of the link.
const offsetTop = currentLink.offsetTop
const parentOffsetTop = parentRef.current?.offsetTop ?? 0
const scrollPosition = offsetTop - parentOffsetTop
parentRef.current?.scrollTo({
top: scrollPosition - 60 /* space for header + padding */,
})
}
const linkOffset =
currentLink.getBoundingClientRect().top - scrollingParent.getBoundingClientRect().top
scrollingParent.scrollTo({
top: scrollingParent.scrollTop + linkOffset - 60 /* space for header + padding */,
})
}, [])
useEffect(() => {
@@ -174,9 +177,9 @@ export function ReferenceNavigationScrollHandler({
}, [scrollActiveIntoView])
return (
<div ref={ref} {...rest}>
<nav ref={ref} {...rest}>
{children}
</div>
</nav>
)
}
@@ -184,12 +187,19 @@ function deriveHref(basePath: string, section: AbbrevApiReferenceSection) {
return 'slug' in section ? `${basePath}/${section.slug}` : ''
}
function getLinkStyles(isActive: boolean, className?: string) {
export function ReferenceActiveCrumb({ basePath, headings }: ReferenceActiveCrumbProps) {
const pathname = useCurrentPathname()
const activePath = pathname === basePath ? `${basePath}/introduction` : pathname
const index = headings.findIndex((heading) => heading.url === activePath)
return <HeadingSlotCrumb heading={headings[index]} index={index} />
}
function getLinkStyles(className?: string) {
return cn(
'text-sm text-foreground-lighter',
!isActive && 'hover:text-foreground',
isActive && 'text-primary',
'transition-colors',
'relative block py-1 text-sm text-foreground-lighter wrap-anywhere',
'transition-colors duration-150 hover:text-foreground-light',
'aria-[current=page]:text-foreground',
className
)
}
@@ -264,9 +274,8 @@ export function RefLink({
useEffect(() => {
if (ref.current) {
ref.current.ariaCurrent = isActive ? 'page' : null
ref.current.className = getLinkStyles(isActive, className)
}
}, [isActive, className])
}, [isActive])
const onClick = useCallback(
(evt: MouseEvent) => {
@@ -292,7 +301,7 @@ export function RefLink({
// pages omit the prop and keep Next.js's default prefetch behavior.
{...(!realNavigation ? { prefetch: false } : {})}
href={href}
className={getLinkStyles(isActive, className)}
className={getLinkStyles(className)}
onClick={onClick}
>
{section.title}
@@ -353,7 +362,7 @@ function CompoundRefLink({
'flex items-center justify-between gap-2'
)}
>
<span className={getLinkStyles(false)}>{section.title}</span>
<span className={getLinkStyles()}>{section.title}</span>
<NavSectionCaret className="group-disabled:cursor-not-allowed group-disabled:opacity-10" />
</button>
</Collapsible.Trigger>
@@ -366,7 +375,7 @@ function CompoundRefLink({
<RefLink
basePath={basePath}
section={item}
className="block py-1.25"
className={ACTIVE_BAR_CLASS_NAME}
realNavigation={realNavigation}
/>
</li>
@@ -1,16 +1,15 @@
import { isFeatureEnabled } from 'common'
import { type PropsWithChildren } from 'react'
import { cn } from 'ui'
import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker'
import RefVersionDropdown from '~/components/RefVersionDropdown'
import { getReferenceSections } from '~/features/docs/Reference.generated.singleton'
import {
RefLink,
ACTIVE_BAR_CLASS_NAME,
ReferenceActiveCrumb,
ReferenceNavigationScrollHandler,
RefLink,
type ReferenceCrumbHeading,
} from '~/features/docs/Reference.navigation.client'
import { type AbbrevApiReferenceSection } from '~/features/docs/Reference.utils'
import { isFeatureEnabled } from 'common'
interface ReferenceNavigationProps {
libraryId: string
@@ -23,8 +22,27 @@ interface ReferenceNavigationProps {
// instead of scrolling within one giant page. SDK/CLI/self-hosting callers
// never pass this, so their behavior is unchanged.
realNavigation?: boolean
// only one of the two rendered copies owns the header crumb
hasActiveCrumb?: boolean
}
interface RefCategoryProps {
basePath: string
section: AbbrevApiReferenceSection
realNavigation?: boolean
}
const flattenSections = (sections: AbbrevApiReferenceSection[]): AbbrevApiReferenceSection[] =>
sections.flatMap((section) => [section, ...flattenSections(section.items ?? [])])
const getCrumbHeadings = (basePath: string, sections: AbbrevApiReferenceSection[]) =>
flattenSections(sections).reduce<ReferenceCrumbHeading[]>((headings, section) => {
if (section.slug && section.title) {
headings.push({ url: `${basePath}/${section.slug}`, title: section.title })
}
return headings
}, [])
export async function ReferenceNavigation({
libraryId,
name,
@@ -33,6 +51,7 @@ export async function ReferenceNavigation({
version,
isLatestVersion,
realNavigation,
hasActiveCrumb = false,
}: ReferenceNavigationProps) {
const navSections = await getReferenceSections(libraryId, version)
const filteredNavSections = navSections?.filter((section) => section.title !== 'Auth')
@@ -41,20 +60,31 @@ export async function ReferenceNavigation({
const basePath = `/reference/${libPath}${isLatestVersion ? '' : `/${version}`}`
return (
<ReferenceNavigationScrollHandler className="w-full flex flex-col pt-3 pb-5 gap-3">
<div className="flex items-center gap-3">
{'icon' in menuData && <MenuIconPicker icon={menuData.icon || ''} width={21} height={21} />}
<span className="text-base text-brand-600">{name}</span>
<ReferenceNavigationScrollHandler
aria-label={`${name} reference`}
className="w-full flex flex-col gap-4"
>
{hasActiveCrumb && displayedNavSections ? (
<ReferenceActiveCrumb
basePath={basePath}
headings={getCrumbHeadings(basePath, displayedNavSections)}
/>
) : null}
<div className="flex items-center gap-2">
{'icon' in menuData ? (
<MenuIconPicker icon={menuData.icon || ''} width={16} height={16} />
) : null}
<span className="text-sm font-medium text-foreground">{name}</span>
<RefVersionDropdown library={libPath} currentVersion={version} />
</div>
<ul className="flex flex-col gap-2">
{displayedNavSections?.map((section, index) =>
<ul className="flex flex-col gap-1">
{displayedNavSections?.map((section) =>
section.type === 'category' ? (
<li key={section.id ?? String(index)}>
<li key={section.id}>
<RefCategory basePath={basePath} section={section} realNavigation={realNavigation} />
</li>
) : (
<li key={section.id ?? String(index)} className={topLvlRefNavItemStyles}>
<li key={section.id}>
<RefLink basePath={basePath} section={section} realNavigation={realNavigation} />
</li>
)
@@ -64,47 +94,28 @@ export async function ReferenceNavigation({
)
}
const topLvlRefNavItemStyles = 'leading-5'
function RefCategory({
basePath,
section,
realNavigation,
}: {
basePath: string
section: AbbrevApiReferenceSection
realNavigation?: boolean
}) {
function RefCategory({ basePath, section, realNavigation }: RefCategoryProps) {
if (!('items' in section && section.items && section.items.length > 0)) return null
return (
<>
<Divider />
{'title' in section && <SideMenuTitle className="py-2">{section.title}</SideMenuTitle>}
<ul className="space-y-2">
<div className="pt-2">
{'title' in section ? (
<span className="block py-1 text-sm font-medium text-foreground-light">
{section.title}
</span>
) : null}
<ul className="mt-1 ml-px border-l pl-3">
{section.items?.map((item) => (
<li key={item.id} className={topLvlRefNavItemStyles}>
<RefLink basePath={basePath} section={item} realNavigation={realNavigation} />
<li key={item.id}>
<RefLink
basePath={basePath}
section={item}
className={ACTIVE_BAR_CLASS_NAME}
realNavigation={realNavigation}
/>
</li>
))}
</ul>
</>
)
}
function Divider() {
return <hr className="w-full h-px my-3 bg-control" />
}
function SideMenuTitle({ children, className }: PropsWithChildren<{ className?: string }>) {
return (
<div
className={cn(
'font-mono font-medium text-xs text-foreground tracking-wider uppercase',
className
)}
>
{children}
</div>
)
}
+11 -10
View File
@@ -20,6 +20,15 @@ export async function ClientSdkReferencePage({ sdkId, libVersion }: ClientSdkRef
const menuData = NavItems[libraryMeta.meta[libVersion].libId]
const navigationProps = {
libraryId: sdkId,
name: menuData.title,
menuData: menuData,
libPath: libraryMeta.libPath,
version: libVersion,
isLatestVersion: isLatestVersion,
}
return (
<ReferenceContentScrollHandler
libPath={libraryMeta.libPath}
@@ -28,16 +37,8 @@ export async function ClientSdkReferencePage({ sdkId, libVersion }: ClientSdkRef
>
<SidebarSkeleton
menuId={libraryMeta.meta[libVersion].libId}
NavigationMenu={
<ReferenceNavigation
libraryId={sdkId}
name={menuData.title}
menuData={menuData}
libPath={libraryMeta.libPath}
version={libVersion}
isLatestVersion={isLatestVersion}
/>
}
NavigationMenu={<ReferenceNavigation {...navigationProps} />}
rightRail={<ReferenceNavigation {...navigationProps} hasActiveCrumb />}
>
<LayoutMainContent>
{!isLatestVersion && (
@@ -47,20 +47,21 @@ export async function SelfHostingReferencePage({
const name = REFERENCES[servicePath.replaceAll('-', '_')].name
const navigationProps = {
libraryId: servicePath,
name: name,
menuData: menuData,
libPath: servicePath,
version: 'latest',
isLatestVersion: true,
}
return (
<ReferenceContentScrollHandler libPath={servicePath} version="latest" isLatestVersion={true}>
<SidebarSkeleton
menuId={menuId}
NavigationMenu={
<ReferenceNavigation
libraryId={servicePath}
name={name}
menuData={menuData}
libPath={servicePath}
version="latest"
isLatestVersion={true}
/>
}
NavigationMenu={<ReferenceNavigation {...navigationProps} />}
rightRail={<ReferenceNavigation {...navigationProps} hasActiveCrumb />}
>
<LayoutMainContent>
<article className="@container/article">