mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
feat(docs): move reference navigation into right rail toc
This commit is contained in:
1 parent
695b5443b0
commit
e4e88d34ab
6 files changed
+151
-127
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user