UI: use fixed positing for TOC

This commit is contained in:
Fuma Nama
2025-08-28 12:11:01 +08:00
parent 2f6f4a56bb
commit 302cdc2fde
5 changed files with 116 additions and 139 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-ui': patch
---
Use `position: fixed` for TOC
+70 -105
View File
@@ -1,18 +1,4 @@
import type { Metadata } from 'next';
import {
PageArticle,
PageBreadcrumb,
PageFooter,
PageLastUpdate,
PageRoot,
PageTOC,
PageTOCItems,
PageTOCPopover,
PageTOCPopoverContent,
PageTOCPopoverItems,
PageTOCPopoverTrigger,
PageTOCTitle,
} from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { type ComponentProps, type FC, type ReactNode } from 'react';
import * as Twoslash from 'fumadocs-twoslash/ui';
@@ -43,6 +29,7 @@ import { Banner } from 'fumadocs-ui/components/banner';
import { openapi } from '@/lib/openapi';
import { Installation } from '@/components/preview/installation';
import { Customisation } from '@/components/preview/customisation';
import { DocsPage } from 'fumadocs-ui/page';
function PreviewRenderer({ preview }: { preview: string }): ReactNode {
if (preview && preview in Preview) {
@@ -67,101 +54,79 @@ export default async function Page(props: PageProps<'/docs/[...slug]'>) {
const { body: Mdx, toc, lastModified } = await page.data.load();
return (
<PageRoot
toc={{
toc,
single: false,
<DocsPage
toc={toc}
lastUpdate={lastModified}
tableOfContent={{
style: 'clerk',
}}
>
{toc.length > 0 && (
<PageTOCPopover>
<PageTOCPopoverTrigger />
<PageTOCPopoverContent>
<PageTOCPopoverItems />
</PageTOCPopoverContent>
</PageTOCPopover>
)}
<PageArticle>
<PageBreadcrumb />
<h1 className="text-3xl font-semibold">{page.data.title}</h1>
<p className="text-lg text-fd-muted-foreground">
{page.data.description}
</p>
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptions
markdownUrl={`${page.url}.mdx`}
githubUrl={`https://github.com/${owner}/${repo}/blob/dev/apps/docs/content/docs/${page.path}`}
/>
</div>
<div className="prose flex-1 text-fd-foreground/80">
{preview ? <PreviewRenderer preview={preview} /> : null}
<Mdx
components={getMDXComponents({
...Twoslash,
a: ({ href, ...props }) => {
const found = source.getPageByHref(href ?? '', {
dir: path.dirname(page.path),
});
<h1 className="text-3xl font-semibold">{page.data.title}</h1>
<p className="text-lg text-fd-muted-foreground">
{page.data.description}
</p>
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptions
markdownUrl={`${page.url}.mdx`}
githubUrl={`https://github.com/${owner}/${repo}/blob/dev/apps/docs/content/docs/${page.path}`}
/>
</div>
<div className="prose flex-1 text-fd-foreground/80">
{preview ? <PreviewRenderer preview={preview} /> : null}
<Mdx
components={getMDXComponents({
...Twoslash,
a: ({ href, ...props }) => {
const found = source.getPageByHref(href ?? '', {
dir: path.dirname(page.path),
});
if (!found) return <Link href={href} {...props} />;
if (!found) return <Link href={href} {...props} />;
return (
<HoverCard>
<HoverCardTrigger asChild>
<Link
href={
found.hash
? `${found.page.url}#${found.hash}`
: found.page.url
}
{...props}
/>
</HoverCardTrigger>
<HoverCardContent className="text-sm">
<p className="font-medium">{found.page.data.title}</p>
<p className="text-fd-muted-foreground">
{found.page.data.description}
</p>
</HoverCardContent>
</HoverCard>
);
},
Banner,
Mermaid,
TypeTable,
AutoTypeTable: (props) => (
<AutoTypeTable generator={generator} {...props} />
),
Wrapper,
blockquote: Callout as unknown as FC<
ComponentProps<'blockquote'>
>,
APIPage: (props) => (
<APIPage {...openapi.getAPIPageProps(props)} />
),
DocsCategory: ({ url }) => {
return <DocsCategory url={url ?? page.url} />;
},
Installation,
Customisation,
...(await import('@/content/docs/ui/components/tabs.client')),
...(await import('@/content/docs/ui/theme.client')),
})}
/>
{page.data.index ? <DocsCategory url={page.url} /> : null}
</div>
<Rate onRateAction={onRateAction} />
{lastModified && <PageLastUpdate date={lastModified} />}
<PageFooter />
</PageArticle>
{toc.length > 0 && (
<PageTOC>
<PageTOCTitle />
<PageTOCItems variant="clerk" />
</PageTOC>
)}
</PageRoot>
return (
<HoverCard>
<HoverCardTrigger asChild>
<Link
href={
found.hash
? `${found.page.url}#${found.hash}`
: found.page.url
}
{...props}
/>
</HoverCardTrigger>
<HoverCardContent className="text-sm">
<p className="font-medium">{found.page.data.title}</p>
<p className="text-fd-muted-foreground">
{found.page.data.description}
</p>
</HoverCardContent>
</HoverCard>
);
},
Banner,
Mermaid,
TypeTable,
AutoTypeTable: (props) => (
<AutoTypeTable generator={generator} {...props} />
),
Wrapper,
blockquote: Callout as unknown as FC<ComponentProps<'blockquote'>>,
APIPage: (props) => <APIPage {...openapi.getAPIPageProps(props)} />,
DocsCategory: ({ url }) => {
return <DocsCategory url={url ?? page.url} />;
},
Installation,
Customisation,
...(await import('@/content/docs/ui/components/tabs.client')),
...(await import('@/content/docs/ui/theme.client')),
})}
/>
{page.data.index ? <DocsCategory url={page.url} /> : null}
</div>
<Rate onRateAction={onRateAction} />
</DocsPage>
);
}
+4 -7
View File
@@ -29,7 +29,7 @@ import {
} from '@/components/ui/collapsible';
import { useSidebar } from '@/contexts/sidebar';
import { useTOCItems } from '@/components/layout/toc';
import { type AnchorProviderProps, useActiveAnchor } from 'fumadocs-core/toc';
import { useActiveAnchor } from 'fumadocs-core/toc';
const TocPopoverContext = createContext<{
open: boolean;
@@ -217,10 +217,6 @@ export function PageTOCPopover(props: ComponentProps<'div'>) {
);
}
export interface RootProps extends ComponentProps<'div'> {
toc: Omit<AnchorProviderProps, 'children'>;
}
export function PageLastUpdate({
date: value,
...props
@@ -399,11 +395,12 @@ export function PageTOC(props: ComponentProps<'div'>) {
<div
id="nd-toc"
{...props}
className={cn('sticky pb-2 pt-12 max-xl:hidden', props.className)}
className={cn('fixed bottom-0 pb-2 pt-12 max-xl:hidden', props.className)}
style={{
...props.style,
top: 'calc(var(--fd-banner-height) + var(--fd-nav-height))',
height: 'calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))',
insetInlineEnd:
'max(var(--fd-layout-offset), calc((100vw - var(--fd-sidebar-width) - var(--fd-page-width))/2))',
}}
>
<div className="flex h-full w-(--fd-toc-width) max-w-full flex-col pe-4">
+20 -16
View File
@@ -10,12 +10,12 @@ import {
PageTOCPopover,
PageTOCPopoverContent,
PageTOCPopoverTrigger,
type RootProps,
} from './page-client';
import { TOCItems, TOCProvider, TOCScrollArea } from '@/components/layout/toc';
import { Text } from 'lucide-react';
import { I18nLabel } from '@/contexts/i18n';
import ClerkTOCItems from '@/components/layout/toc-clerk';
import type { AnchorProviderProps } from 'fumadocs-core/toc';
/**
* Apply `prose` on div
@@ -79,21 +79,26 @@ export function PageArticle(props: ComponentProps<'article'>) {
);
}
export function PageRoot({ toc, children, ...props }: RootProps) {
return (
<TOCProvider {...toc}>
<div
id="nd-page"
{...props}
className={cn(
'flex flex-1 w-full mx-auto max-w-(--fd-page-width) pt-(--fd-tocnav-height)',
props.className,
)}
>
{children}
</div>
</TOCProvider>
export interface RootProps extends ComponentProps<'div'> {
toc?: Omit<AnchorProviderProps, 'children'> | false;
}
export function PageRoot({ toc = false, children, ...props }: RootProps) {
const content = (
<div
id="nd-page"
{...props}
className={cn(
'flex flex-1 w-full mx-auto max-w-(--fd-page-width) pt-(--fd-tocnav-height) pe-(--fd-toc-width)',
props.className,
)}
>
{children}
</div>
);
if (toc) return <TOCProvider {...toc}>{content}</TOCProvider>;
return content;
}
export {
@@ -106,5 +111,4 @@ export {
PageTOCPopoverContent,
type FooterProps,
type BreadcrumbProps,
type RootProps,
};
+17 -11
View File
@@ -135,13 +135,12 @@ export function DocsPage({
article,
children,
}: DocsPageProps) {
const isTocRequired =
toc.length > 0 ||
tocOptions.footer !== undefined ||
tocOptions.header !== undefined;
// disable TOC on full mode, you can still enable it with `enabled` option.
tocEnabled ??= !full && isTocRequired;
tocEnabled ??=
!full &&
(toc.length > 0 ||
tocOptions.footer !== undefined ||
tocOptions.header !== undefined);
tocPopoverEnabled ??=
toc.length > 0 ||
@@ -150,11 +149,19 @@ export function DocsPage({
return (
<PageRoot
toc={{
toc,
single: tocOptions.single,
}}
toc={
tocEnabled || tocPopoverEnabled
? {
toc,
single: tocOptions.single,
}
: false
}
{...container}
className={cn(
!tocEnabled && '[--fd-toc-width:0px]',
container?.className,
)}
>
{tocPopoverEnabled &&
(tocPopover ?? (
@@ -171,7 +178,6 @@ export function DocsPage({
{breadcrumbEnabled &&
(breadcrumb ?? <PageBreadcrumb {...breadcrumbProps} />)}
{children}
<div role="none" className="flex-1" />
<div className="flex flex-row flex-wrap items-center justify-between gap-4 empty:hidden">
{editOnGithub && (
<EditOnGitHub