mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
UI: Move TOC closer to page body on larger viewports
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'fumadocs-ui': minor
|
||||
---
|
||||
|
||||
**Move TOC closer to page body on larger viewports**
|
||||
|
||||
Note that this may impact sites using custom styling.
|
||||
|
||||
For Fumadocs CLI users: you can re-install the layout components again to get the latest look.
|
||||
@@ -54,7 +54,7 @@ export function LLMCopyButton({ slug }: { slug: string[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function EditOnGitHub({ url }: { url: string }) {
|
||||
export function GitHubLink({ url }: { url: string }) {
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
@@ -75,7 +75,7 @@ export function EditOnGitHub({ url }: { url: string }) {
|
||||
<title>GitHub</title>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
Edit on GitHub
|
||||
View on GitHub
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { getPageTreePeers } from 'fumadocs-core/server';
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
import { APIPage } from 'fumadocs-openapi/ui';
|
||||
import { EditOnGitHub, LLMCopyButton } from './page.client';
|
||||
import { GitHubLink, LLMCopyButton } from './page.client';
|
||||
|
||||
function PreviewRenderer({ preview }: { preview: string }): ReactNode {
|
||||
if (preview && preview in Preview) {
|
||||
@@ -79,9 +79,9 @@ export default async function Page(props: {
|
||||
<DocsDescription className="mb-0">
|
||||
{page.data.description}
|
||||
</DocsDescription>
|
||||
<div className="flex flex-row gap-2 items-center mb-4">
|
||||
<div className="flex flex-row gap-2 items-center mb-8 border-b pb-6">
|
||||
<LLMCopyButton slug={params.slug} />
|
||||
<EditOnGitHub
|
||||
<GitHubLink
|
||||
url={`https://github.com/${owner}/${repo}/blob/dev/${path}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
@theme {
|
||||
--spacing-fd-container: 1400px;
|
||||
--fd-page-width: 1200px;
|
||||
--fd-sidebar-width: 0px;
|
||||
--fd-toc-width: 0px;
|
||||
--fd-layout-width: 100vw;
|
||||
@@ -242,6 +243,11 @@
|
||||
[data-rmiz-modal-overlay='visible'] {
|
||||
background-color: var(--color-fd-background);
|
||||
}
|
||||
|
||||
:root,
|
||||
#nd-docs-layout {
|
||||
--fd-layout-offset: max(calc(50vw - var(--fd-layout-width) / 2), 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@utility fd-scroll-container {
|
||||
|
||||
@@ -150,7 +150,7 @@ export function Sidebar({
|
||||
{...props}
|
||||
data-collapsed={collapsed}
|
||||
className={cn(
|
||||
'sticky shrink-0 flex flex-col items-end top-(--fd-sidebar-top) z-20 bg-fd-card text-sm h-(--fd-sidebar-height) w-(--fd-sidebar-width) *:w-(--fd-sidebar-width) border-e max-md:hidden',
|
||||
'fixed start-0 flex flex-col items-end top-(--fd-sidebar-top) bottom-(--fd-sidebar-margin) z-20 bg-fd-card text-sm border-e max-md:hidden *:w-(--fd-sidebar-width)',
|
||||
collapsed && [
|
||||
'rounded-xl border',
|
||||
hover
|
||||
@@ -161,15 +161,16 @@ export function Sidebar({
|
||||
)}
|
||||
style={
|
||||
{
|
||||
transition: ['top', 'opacity', 'margin', 'translate', 'width']
|
||||
.map((v) => `${v} ease 200ms`)
|
||||
transition: ['top', 'opacity', 'translate', 'width']
|
||||
.map((v) => `${v} ease 250ms`)
|
||||
.join(', '),
|
||||
...props.style,
|
||||
'--fd-sidebar-offset': 'calc(100% - 16px)',
|
||||
'--fd-sidebar-margin': collapsed ? '0.5rem' : '0px',
|
||||
width: collapsed
|
||||
? 'var(--fd-sidebar-width)'
|
||||
: 'calc(var(--fd-sidebar-width) + var(--fd-layout-offset))',
|
||||
'--fd-sidebar-top': `calc(var(--fd-banner-height) + var(--fd-nav-height) + var(--fd-sidebar-margin))`,
|
||||
'--fd-sidebar-height':
|
||||
'calc(100dvh - var(--fd-sidebar-top) - var(--fd-sidebar-margin))',
|
||||
...props.style,
|
||||
} as object
|
||||
}
|
||||
onPointerEnter={(e) => {
|
||||
|
||||
@@ -28,6 +28,29 @@ export function Navbar(props: ComponentProps<'header'>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutBody(props: ComponentProps<'main'>) {
|
||||
const { collapsed } = useSidebar();
|
||||
|
||||
return (
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col transition-[margin]',
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
...props.style,
|
||||
marginInlineStart: collapsed
|
||||
? 'max(0px, min(calc(100vw - var(--fd-page-width)), var(--fd-sidebar-width)))'
|
||||
: 'var(--fd-sidebar-width)',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavbarSidebarTrigger({
|
||||
className,
|
||||
...props
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '@/components/layout/language-toggle';
|
||||
import {
|
||||
CollapsibleControl,
|
||||
LayoutBody,
|
||||
Navbar,
|
||||
NavbarSidebarTrigger,
|
||||
} from '@/layouts/docs-client';
|
||||
@@ -32,7 +33,6 @@ import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { ThemeToggle } from '@/components/layout/theme-toggle';
|
||||
import {
|
||||
getSidebarTabsFromOptions,
|
||||
layoutVariables,
|
||||
SidebarLinkItem,
|
||||
type SidebarOptions,
|
||||
} from '@/layouts/docs/shared';
|
||||
@@ -111,18 +111,9 @@ export function DocsLayout({
|
||||
<NavbarSidebarTrigger className="p-2 -me-1.5 md:hidden" />
|
||||
</Navbar>,
|
||||
)}
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
<LayoutBody
|
||||
{...props.containerProps}
|
||||
className={cn(
|
||||
'flex flex-1 flex-row pe-(--fd-layout-offset)',
|
||||
variables,
|
||||
props.containerProps?.className,
|
||||
)}
|
||||
style={{
|
||||
...layoutVariables,
|
||||
...props.containerProps?.style,
|
||||
}}
|
||||
className={cn(variables, props.containerProps?.className)}
|
||||
>
|
||||
{slot(
|
||||
sidebar,
|
||||
@@ -167,7 +158,7 @@ export function DocsLayout({
|
||||
/>,
|
||||
)}
|
||||
<StylesProvider {...pageStyles}>{children}</StylesProvider>
|
||||
</main>
|
||||
</LayoutBody>
|
||||
</NavProvider>
|
||||
</TreeContextProvider>
|
||||
);
|
||||
@@ -188,14 +179,7 @@ export function DocsLayoutSidebar({
|
||||
return (
|
||||
<>
|
||||
{collapsible ? <CollapsibleControl /> : null}
|
||||
<Sidebar
|
||||
{...props}
|
||||
collapsible={collapsible}
|
||||
className={cn(
|
||||
'data-[collapsed=false]:w-[calc(var(--fd-sidebar-width)+var(--fd-layout-offset))] data-[collapsed=true]:me-[calc(var(--fd-layout-offset)-var(--fd-sidebar-width))]',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<Sidebar {...props} collapsible={collapsible}>
|
||||
<HideIfEmpty>
|
||||
<SidebarHeader>
|
||||
<div className="flex max-md:hidden">
|
||||
|
||||
@@ -16,10 +16,6 @@ import {
|
||||
type GetSidebarTabsOptions,
|
||||
} from '@/utils/get-sidebar-tabs';
|
||||
|
||||
export const layoutVariables = {
|
||||
'--fd-layout-offset': 'max(calc(50vw - var(--fd-layout-width) / 2), 0px)',
|
||||
};
|
||||
|
||||
export interface SidebarOptions extends SidebarProps {
|
||||
components?: Partial<SidebarComponents>;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type ButtonHTMLAttributes, type HTMLAttributes } from 'react';
|
||||
import { type ComponentProps } from 'react';
|
||||
import { useSidebar } from '@/contexts/sidebar';
|
||||
import { useNav } from '@/contexts/layout';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
@@ -13,7 +13,7 @@ import type { Option } from '@/components/layout/root-toggle';
|
||||
export function Navbar({
|
||||
mode,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLElement> & { mode: 'top' | 'auto' }) {
|
||||
}: ComponentProps<'header'> & { mode: 'top' | 'auto' }) {
|
||||
const { open, collapsed } = useSidebar();
|
||||
const { isTransparent } = useNav();
|
||||
|
||||
@@ -35,10 +35,33 @@ export function Navbar({
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutBody(props: ComponentProps<'main'>) {
|
||||
const { collapsed } = useSidebar();
|
||||
|
||||
return (
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col transition-[margin]',
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
...props.style,
|
||||
marginInlineStart: collapsed
|
||||
? 'max(0px, min(calc(100vw - var(--fd-page-width)), var(--fd-sidebar-width)))'
|
||||
: 'var(--fd-sidebar-width)',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavbarSidebarTrigger({
|
||||
className,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
}: ComponentProps<'button'>) {
|
||||
const { setOpen } = useSidebar();
|
||||
|
||||
return (
|
||||
@@ -58,7 +81,7 @@ export function NavbarSidebarTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutTabs(props: HTMLAttributes<HTMLElement>) {
|
||||
export function LayoutTabs(props: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
@@ -72,17 +95,12 @@ export function LayoutTabs(props: HTMLAttributes<HTMLElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
function useIsSelected(item: Option) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return item.urls
|
||||
? item.urls.has(pathname.endsWith('/') ? pathname.slice(0, -1) : pathname)
|
||||
: isActive(item.url, pathname, true);
|
||||
}
|
||||
|
||||
export function LayoutTab(item: Option) {
|
||||
const { closeOnRedirect } = useSidebar();
|
||||
const selected = useIsSelected(item);
|
||||
const pathname = usePathname();
|
||||
const selected = item.urls
|
||||
? item.urls.has(pathname.endsWith('/') ? pathname.slice(0, -1) : pathname)
|
||||
: isActive(item.url, pathname, true);
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -22,12 +22,12 @@ import {
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
getSidebarTabsFromOptions,
|
||||
layoutVariables,
|
||||
SidebarLinkItem,
|
||||
type SidebarOptions,
|
||||
} from '@/layouts/docs/shared';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import {
|
||||
LayoutBody,
|
||||
LayoutTab,
|
||||
LayoutTabs,
|
||||
Navbar,
|
||||
@@ -120,23 +120,13 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
return (
|
||||
<TreeContextProvider tree={props.tree}>
|
||||
<NavProvider transparentMode={transparentMode}>
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
<LayoutBody
|
||||
{...props.containerProps}
|
||||
className={cn(
|
||||
'flex w-full flex-1 flex-row pe-(--fd-layout-offset)',
|
||||
variables,
|
||||
props.containerProps?.className,
|
||||
)}
|
||||
style={{
|
||||
...layoutVariables,
|
||||
...props.containerProps?.style,
|
||||
}}
|
||||
className={cn(variables, props.containerProps?.className)}
|
||||
>
|
||||
<Sidebar
|
||||
{...sidebar}
|
||||
className={cn(
|
||||
'data-[collapsed=false]:w-[calc(var(--fd-sidebar-width)+var(--fd-layout-offset))] data-[collapsed=true]:me-[calc(var(--fd-layout-offset)-var(--fd-sidebar-width))]',
|
||||
navMode === 'top'
|
||||
? 'md:bg-transparent'
|
||||
: 'md:[--fd-nav-height:0px]',
|
||||
@@ -216,7 +206,7 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
tabs={tabMode == 'navbar' ? tabs : []}
|
||||
/>
|
||||
<StylesProvider {...pageStyles}>{props.children}</StylesProvider>
|
||||
</main>
|
||||
</LayoutBody>
|
||||
</NavProvider>
|
||||
</TreeContextProvider>
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import * as Primitive from 'fumadocs-core/toc';
|
||||
import { useSidebar } from '@/contexts/sidebar';
|
||||
|
||||
const TocPopoverContext = createContext<{
|
||||
open: boolean;
|
||||
@@ -160,6 +161,7 @@ export function TocPopoverContent(props: ComponentProps<'div'>) {
|
||||
export function TocPopover(props: ComponentProps<'div'>) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { collapsed } = useSidebar();
|
||||
const { tocNav } = usePageStyles();
|
||||
const { isTransparent } = useNav();
|
||||
|
||||
@@ -181,10 +183,16 @@ export function TocPopover(props: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('sticky overflow-visible z-10', tocNav, props.className)}
|
||||
className={cn(
|
||||
'fixed inset-x-0 top-[calc(var(--fd-banner-height)+var(--fd-nav-height))] overflow-visible z-10',
|
||||
tocNav,
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
...props.style,
|
||||
top: 'calc(var(--fd-banner-height) + var(--fd-nav-height))',
|
||||
insetInlineStart: collapsed
|
||||
? '0px'
|
||||
: 'calc(var(--fd-sidebar-width) + var(--fd-layout-offset))',
|
||||
}}
|
||||
>
|
||||
<TocPopoverContext.Provider
|
||||
@@ -215,16 +223,28 @@ export function TocPopover(props: ComponentProps<'div'>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PageBody(props: ComponentProps<'div'>) {
|
||||
export function PageBody({
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<'div'>) {
|
||||
const { page } = usePageStyles();
|
||||
const { collapsed } = useSidebar();
|
||||
|
||||
return (
|
||||
<div
|
||||
id="nd-page"
|
||||
{...props}
|
||||
className={cn('flex w-full min-w-0 flex-col', page, props.className)}
|
||||
className={cn(className, page)}
|
||||
style={{
|
||||
maxWidth: collapsed
|
||||
? 'var(--fd-page-width)'
|
||||
: 'min(var(--fd-page-width),calc(var(--fd-layout-width) - var(--fd-sidebar-width)))',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -236,7 +256,7 @@ export function PageArticle(props: ComponentProps<'article'>) {
|
||||
<article
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex w-full flex-1 flex-col gap-6 px-4 md:px-6 pt-8 md:pt-12 xl:px-12 xl:mx-auto',
|
||||
'flex min-w-0 w-full flex-col gap-6 px-4 pt-8 md:px-6 md:pt-12 xl:px-12 xl:mx-auto',
|
||||
article,
|
||||
props.className,
|
||||
)}
|
||||
|
||||
+46
-51
@@ -138,9 +138,31 @@ export function DocsPage({
|
||||
|
||||
return (
|
||||
<AnchorProvider toc={toc} single={tocOptions.single}>
|
||||
{slot(
|
||||
{ enabled: tocPopoverEnabled, component: tocPopoverReplace },
|
||||
<TocPopover className="h-10">
|
||||
<TocPopoverTrigger className="w-full" items={toc} />
|
||||
<TocPopoverContent>
|
||||
{tocPopoverOptions.header}
|
||||
<TOCScrollArea className="px-4 md:px-6">
|
||||
{tocPopoverOptions.style === 'clerk' ? (
|
||||
<ClerkTOCItems items={toc} />
|
||||
) : (
|
||||
<TOCItems items={toc} />
|
||||
)}
|
||||
</TOCScrollArea>
|
||||
{tocPopoverOptions.footer}
|
||||
</TocPopoverContent>
|
||||
</TocPopover>,
|
||||
{
|
||||
items: toc,
|
||||
...tocPopoverOptions,
|
||||
},
|
||||
)}
|
||||
|
||||
<PageBody
|
||||
{...props.container}
|
||||
className={cn(props.container?.className)}
|
||||
className={cn('flex flex-1 mx-auto w-full', props.container?.className)}
|
||||
style={
|
||||
{
|
||||
'--fd-tocnav-height': !tocPopoverEnabled ? '0px' : undefined,
|
||||
@@ -148,34 +170,7 @@ export function DocsPage({
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{slot(
|
||||
{ enabled: tocPopoverEnabled, component: tocPopoverReplace },
|
||||
<TocPopover className="h-10">
|
||||
<TocPopoverTrigger className="w-full" items={toc} />
|
||||
<TocPopoverContent>
|
||||
{tocPopoverOptions.header}
|
||||
<TOCScrollArea className="px-4 md:px-6">
|
||||
{tocPopoverOptions.style === 'clerk' ? (
|
||||
<ClerkTOCItems items={toc} />
|
||||
) : (
|
||||
<TOCItems items={toc} />
|
||||
)}
|
||||
</TOCScrollArea>
|
||||
{tocPopoverOptions.footer}
|
||||
</TocPopoverContent>
|
||||
</TocPopover>,
|
||||
{
|
||||
items: toc,
|
||||
...tocPopoverOptions,
|
||||
},
|
||||
)}
|
||||
<PageArticle
|
||||
{...props.article}
|
||||
className={cn(
|
||||
full || !tocEnabled ? 'max-w-[1120px]' : 'max-w-[860px]',
|
||||
props.article?.className,
|
||||
)}
|
||||
>
|
||||
<PageArticle {...props.article}>
|
||||
{slot(props.breadcrumb, <Breadcrumb {...props.breadcrumb} />)}
|
||||
{props.children}
|
||||
<div role="none" className="flex-1" />
|
||||
@@ -191,29 +186,29 @@ export function DocsPage({
|
||||
</div>
|
||||
{slot(props.footer, <Footer items={props.footer?.items} />)}
|
||||
</PageArticle>
|
||||
{slot(
|
||||
{ enabled: tocEnabled, component: tocReplace },
|
||||
<Toc>
|
||||
{tocOptions.header}
|
||||
<h3 className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground">
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
{tocOptions.style === 'clerk' ? (
|
||||
<ClerkTOCItems items={toc} />
|
||||
) : (
|
||||
<TOCItems items={toc} />
|
||||
)}
|
||||
</TOCScrollArea>
|
||||
{tocOptions.footer}
|
||||
</Toc>,
|
||||
{
|
||||
items: toc,
|
||||
...tocOptions,
|
||||
},
|
||||
)}
|
||||
</PageBody>
|
||||
{slot(
|
||||
{ enabled: tocEnabled, component: tocReplace },
|
||||
<Toc>
|
||||
{tocOptions.header}
|
||||
<h3 className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground">
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
{tocOptions.style === 'clerk' ? (
|
||||
<ClerkTOCItems items={toc} />
|
||||
) : (
|
||||
<TOCItems items={toc} />
|
||||
)}
|
||||
</TOCScrollArea>
|
||||
{tocOptions.footer}
|
||||
</Toc>,
|
||||
{
|
||||
items: toc,
|
||||
...tocOptions,
|
||||
},
|
||||
)}
|
||||
</AnchorProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user