mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
UI: Support fumadocs-ui/layouts/docs-notebook layout
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Support `fumadocs-ui/layouts/docs-notebook` layout
|
||||
@@ -14,21 +14,7 @@ export default {
|
||||
presets: [
|
||||
createPreset({
|
||||
addGlobalColors: true,
|
||||
preset: {
|
||||
...presets.default,
|
||||
dark: {
|
||||
...presets.default.dark,
|
||||
background: '0 0% 2%',
|
||||
foreground: '0 0% 98%',
|
||||
popover: '0 0% 4%',
|
||||
card: '0 0% 4%',
|
||||
muted: '0 0% 8%',
|
||||
border: '0 0% 14%',
|
||||
accent: '0 0% 15%',
|
||||
'accent-foreground': '0 0% 100%',
|
||||
'muted-foreground': '0 0% 60%',
|
||||
},
|
||||
},
|
||||
preset: 'neutral',
|
||||
}),
|
||||
],
|
||||
theme: {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
'use client';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import {
|
||||
type BreadcrumbOptions,
|
||||
getBreadcrumbItemsFromPath,
|
||||
} from 'fumadocs-core/breadcrumb';
|
||||
import Link from 'next/link';
|
||||
import { Fragment, useMemo } from 'react';
|
||||
import { useTreeContext, useTreePath } from '@/contexts/tree';
|
||||
|
||||
export interface BreadcrumbProps
|
||||
extends Omit<BreadcrumbOptions, 'includePage'> {
|
||||
/**
|
||||
* Show the full path to the current page
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export function Breadcrumb({ full = false, ...options }: BreadcrumbProps) {
|
||||
const path = useTreePath();
|
||||
const { root } = useTreeContext();
|
||||
const items = useMemo(() => {
|
||||
return getBreadcrumbItemsFromPath(root, path, {
|
||||
includePage: full,
|
||||
...options,
|
||||
});
|
||||
}, [full, options, path, root]);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="-mb-3 flex flex-row items-center gap-1 text-sm font-medium text-fd-muted-foreground">
|
||||
{items.map((item, i) => (
|
||||
<Fragment key={i}>
|
||||
{i !== 0 && (
|
||||
<ChevronRight className="size-4 shrink-0 rtl:rotate-180" />
|
||||
)}
|
||||
{item.url ? (
|
||||
<Link
|
||||
href={item.url}
|
||||
className="truncate hover:text-fd-accent-foreground"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="truncate">{item.name}</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,13 +34,13 @@ export const NavContext = createContext<NavContextType>({
|
||||
export function NavProvider({
|
||||
transparentMode = 'none',
|
||||
children,
|
||||
}: NavProviderProps & { children: ReactNode }): ReactNode {
|
||||
}: NavProviderProps & { children: ReactNode }) {
|
||||
const [transparent, setTransparent] = useState(transparentMode !== 'none');
|
||||
|
||||
useEffect(() => {
|
||||
if (transparentMode !== 'top') return;
|
||||
|
||||
const listener = (): void => {
|
||||
const listener = () => {
|
||||
setTransparent(window.scrollY < 10);
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ export function Title({
|
||||
title,
|
||||
url,
|
||||
...props
|
||||
}: TitleProps & Omit<LinkProps, 'title'>): React.ReactElement {
|
||||
}: TitleProps & Omit<LinkProps, 'title'>) {
|
||||
const { locale } = useI18n();
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { cn } from '@/utils/cn';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import { TocThumb } from '@/components/layout/toc-thumb';
|
||||
import { ScrollArea, ScrollViewport } from '../ui/scroll-area';
|
||||
import { TocItemsEmpty } from '@/components/layout/toc';
|
||||
|
||||
export default function ClerkTOCItems({
|
||||
items,
|
||||
@@ -70,12 +71,7 @@ export default function ClerkTOCItems({
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
if (items.length === 0)
|
||||
return (
|
||||
<div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
|
||||
{text.tocNoHeadings}
|
||||
</div>
|
||||
);
|
||||
if (items.length === 0) return <TocItemsEmpty />;
|
||||
|
||||
return (
|
||||
<ScrollArea className={cn('flex flex-col', isMenu && '-ms-3')}>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function Toc(props: HTMLAttributes<HTMLDivElement>) {
|
||||
{...props}
|
||||
data-toc=""
|
||||
className={cn(
|
||||
'sticky top-fd-layout-top h-[var(--fd-toc-height)] flex-1 pb-2 pe-2 pt-12 max-lg:hidden',
|
||||
'sticky top-fd-layout-top h-[var(--fd-toc-height)] flex-1 pb-2 pt-12 max-lg:hidden',
|
||||
props.className,
|
||||
)}
|
||||
style={
|
||||
@@ -43,6 +43,16 @@ export function Toc(props: HTMLAttributes<HTMLDivElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function TocItemsEmpty() {
|
||||
const { text } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
|
||||
{text.tocNoHeadings}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TOCItems({
|
||||
items,
|
||||
isMenu = false,
|
||||
@@ -50,16 +60,10 @@ export function TOCItems({
|
||||
items: TOCItemType[];
|
||||
isMenu?: boolean;
|
||||
}) {
|
||||
const { text } = useI18n();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
if (items.length === 0)
|
||||
return (
|
||||
<div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
|
||||
{text.tocNoHeadings}
|
||||
</div>
|
||||
);
|
||||
if (items.length === 0) return <TocItemsEmpty />;
|
||||
|
||||
return (
|
||||
<ScrollArea className={cn('flex flex-col', isMenu && '-ms-3')}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import {
|
||||
type ComponentType,
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
@@ -43,7 +44,7 @@ export interface SearchProviderProps {
|
||||
*
|
||||
* It receives the `open` and `onOpenChange` prop, can be lazy loaded with `next/dynamic`
|
||||
*/
|
||||
SearchDialog: React.ComponentType<SharedProps>;
|
||||
SearchDialog: ComponentType<SharedProps>;
|
||||
|
||||
/**
|
||||
* Additional props to the dialog
|
||||
@@ -97,7 +98,7 @@ export function SearchProvider({
|
||||
},
|
||||
],
|
||||
links,
|
||||
}: SearchProviderProps): React.ReactElement {
|
||||
}: SearchProviderProps) {
|
||||
const [isOpen, setIsOpen] = useState(preload ? false : undefined);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -138,6 +139,9 @@ export function SearchProvider({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show children only when search is enabled via React Context
|
||||
*/
|
||||
export function SearchOnly({ children }: { children: ReactNode }) {
|
||||
const search = useSearchContext();
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useSidebar } from '@/contexts/sidebar';
|
||||
|
||||
export function LayoutBody(props: HTMLAttributes<HTMLElement>) {
|
||||
const { collapsed } = useSidebar();
|
||||
|
||||
return (
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
{...props}
|
||||
className={cn('flex w-full flex-1 flex-row', props.className)}
|
||||
style={
|
||||
{
|
||||
...props.style,
|
||||
'--fd-content-width': collapsed
|
||||
? '100vw'
|
||||
: 'calc(min(100vw, var(--fd-layout-width)) - var(--fd-sidebar-width))',
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { Fragment, type HTMLAttributes, type ReactNode } from 'react';
|
||||
import {
|
||||
type BaseLayoutProps,
|
||||
getLinks,
|
||||
type SharedNavProps,
|
||||
} from '@/layouts/shared';
|
||||
import {
|
||||
CollapsibleSidebar,
|
||||
Sidebar,
|
||||
SidebarCollapseTrigger,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarViewport,
|
||||
} from '@/layouts/docs/sidebar';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { RootToggle } from '@/components/layout/root-toggle';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { NavProvider, Title } from '@/components/layout/nav';
|
||||
import { Navbar, NavbarSidebarTrigger } from '@/layouts/docs/navbar';
|
||||
import { SearchOnly } from '@/contexts/search';
|
||||
import {
|
||||
LargeSearchToggle,
|
||||
SearchToggle,
|
||||
} from '@/components/layout/search-toggle';
|
||||
import { cn } from '@/utils/cn';
|
||||
import Link from 'next/link';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { ChevronDown, Languages } from 'lucide-react';
|
||||
import { BaseLinkItem, type LinkItemType } from '@/layouts/links';
|
||||
import { LanguageToggle } from '@/components/layout/language-toggle';
|
||||
import { ThemeToggle } from '@/components/layout/theme-toggle';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
getSidebarTabsFromOptions,
|
||||
SidebarLinkItem,
|
||||
type SidebarOptions,
|
||||
} from '@/layouts/docs/shared';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { SidebarItems } from '@/layouts/docs/sidebar-items';
|
||||
import { LayoutBody } from '@/layouts/docs-notebook.client';
|
||||
|
||||
export interface DocsLayoutProps extends Omit<BaseLayoutProps, 'nav'> {
|
||||
tree: PageTree.Root;
|
||||
|
||||
nav: Omit<Exclude<BaseLayoutProps['nav'], undefined>, 'enabled'>;
|
||||
sidebar?: Omit<Partial<SidebarOptions>, 'component' | 'enabled'>;
|
||||
|
||||
containerProps?: HTMLAttributes<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function DocsLayout({
|
||||
nav: { transparentMode, ...nav } = {},
|
||||
sidebar: {
|
||||
collapsible: sidebarCollapsible = true,
|
||||
tabs: tabOptions,
|
||||
banner: sidebarBanner,
|
||||
footer: sidebarFooter,
|
||||
components: sidebarComponents,
|
||||
...sidebar
|
||||
} = {},
|
||||
i18n = false,
|
||||
...props
|
||||
}: DocsLayoutProps): ReactNode {
|
||||
const links = getLinks(props.links ?? [], props.githubUrl);
|
||||
const Aside = sidebarCollapsible ? CollapsibleSidebar : Sidebar;
|
||||
if (props.tree === undefined) notFound();
|
||||
|
||||
const tabs = getSidebarTabsFromOptions(tabOptions, props.tree) ?? [];
|
||||
|
||||
return (
|
||||
<TreeContextProvider tree={props.tree}>
|
||||
<NavProvider transparentMode={transparentMode}>
|
||||
<LayoutBody
|
||||
{...props.containerProps}
|
||||
className={cn(
|
||||
'[--fd-nav-height:3.5rem] md:[--fd-sidebar-width:260px] lg:[--fd-toc-width:240px] xl:[--fd-toc-width:260px]',
|
||||
props.containerProps?.className,
|
||||
)}
|
||||
>
|
||||
<Aside
|
||||
{...sidebar}
|
||||
className={cn('md:[--fd-nav-height:0px]', sidebar.className)}
|
||||
>
|
||||
<SidebarHeader>
|
||||
<SidebarHeaderItems nav={nav} links={links}>
|
||||
{nav.children}
|
||||
{sidebarCollapsible ? (
|
||||
<SidebarCollapseTrigger className="ms-auto text-fd-muted-foreground" />
|
||||
) : null}
|
||||
</SidebarHeaderItems>
|
||||
{sidebarBanner}
|
||||
{tabs.length > 0 ? (
|
||||
<RootToggle options={tabs} className="md:-mx-2" />
|
||||
) : null}
|
||||
</SidebarHeader>
|
||||
<SidebarViewport>
|
||||
<div className="px-4 pt-4 empty:hidden md:px-3 lg:hidden">
|
||||
{links.map((item, i) => (
|
||||
<SidebarLinkItem key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
<div className="p-4 md:px-3">
|
||||
<SidebarItems components={sidebarComponents} />
|
||||
</div>
|
||||
</SidebarViewport>
|
||||
<SidebarFooter>{sidebarFooter}</SidebarFooter>
|
||||
</Aside>
|
||||
<div className="w-full min-w-0 max-w-[var(--fd-content-width)] [--fd-nav-height:3.5rem]">
|
||||
<DocsNavbar
|
||||
nav={nav}
|
||||
links={links}
|
||||
i18n={i18n}
|
||||
sidebarCollapsible={sidebarCollapsible}
|
||||
/>
|
||||
<div className="flex flex-row">{props.children}</div>
|
||||
</div>
|
||||
<div
|
||||
className="min-w-[var(--fd-sidebar-width)] flex-1"
|
||||
style={{
|
||||
marginInlineStart: 'calc(var(--fd-sidebar-width) * -1)',
|
||||
}}
|
||||
/>
|
||||
</LayoutBody>
|
||||
</NavProvider>
|
||||
</TreeContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function DocsNavbar({
|
||||
sidebarCollapsible,
|
||||
links,
|
||||
nav,
|
||||
i18n,
|
||||
}: {
|
||||
nav: DocsLayoutProps['nav'];
|
||||
sidebarCollapsible: boolean;
|
||||
i18n: boolean;
|
||||
links: LinkItemType[];
|
||||
}) {
|
||||
return (
|
||||
<Navbar className="h-14">
|
||||
{sidebarCollapsible ? (
|
||||
<SidebarCollapseTrigger className="me-2 data-[collapsed=false]:hidden max-md:hidden" />
|
||||
) : null}
|
||||
<SearchOnly>
|
||||
<LargeSearchToggle className="w-full max-w-[240px] rounded-lg max-md:hidden" />
|
||||
</SearchOnly>
|
||||
<Title url={nav.url} title={nav.title} className="md:hidden" />
|
||||
<div className="ms-4 flex flex-1 flex-row items-center gap-6">
|
||||
{links
|
||||
.filter((item) => item.type !== 'icon')
|
||||
.map((item, i) => (
|
||||
<NavbarLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className="text-sm text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground max-lg:hidden"
|
||||
/>
|
||||
))}
|
||||
{nav.children}
|
||||
</div>
|
||||
<SearchOnly>
|
||||
<SearchToggle className="md:hidden" />
|
||||
</SearchOnly>
|
||||
<NavbarSidebarTrigger className="-me-2 md:hidden" />
|
||||
{links
|
||||
.filter((item) => item.type === 'icon')
|
||||
.map((item, i) => (
|
||||
<BaseLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({ size: 'icon', color: 'ghost' }),
|
||||
'text-fd-muted-foreground max-lg:hidden',
|
||||
)}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</BaseLinkItem>
|
||||
))}
|
||||
{i18n ? (
|
||||
<LanguageToggle className="me-1.5">
|
||||
<Languages className="size-5" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
<ThemeToggle className="max-md:hidden" />
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
function NavbarLinkItem({
|
||||
item,
|
||||
...props
|
||||
}: { item: LinkItemType } & HTMLAttributes<HTMLElement>) {
|
||||
if (item.type === 'menu') {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
className={cn('inline-flex items-center gap-1.5', props.className)}
|
||||
>
|
||||
{item.text}
|
||||
<ChevronDown className="size-3" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex flex-col">
|
||||
{item.items.map((child, i) => {
|
||||
if (child.type === 'custom')
|
||||
return <Fragment key={i}>{child.children}</Fragment>;
|
||||
|
||||
return (
|
||||
<BaseLinkItem
|
||||
key={i}
|
||||
item={child}
|
||||
className="inline-flex items-center gap-2 rounded-md p-2 text-start hover:bg-fd-accent hover:text-fd-accent-foreground data-[active=true]:text-fd-primary [&_svg]:size-4"
|
||||
>
|
||||
{child.icon}
|
||||
{child.text}
|
||||
</BaseLinkItem>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === 'custom') return item.children;
|
||||
|
||||
return (
|
||||
<BaseLinkItem item={item} {...props}>
|
||||
{item.text}
|
||||
</BaseLinkItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarHeaderItems({
|
||||
links,
|
||||
nav,
|
||||
children,
|
||||
}: SharedNavProps & {
|
||||
nav: DocsLayoutProps['nav'];
|
||||
links: LinkItemType[];
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const isEmpty = !nav.title && !nav.children && links.length === 0;
|
||||
if (isEmpty) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center max-md:hidden">
|
||||
{nav.title ? (
|
||||
<Link
|
||||
href={nav.url ?? '/'}
|
||||
className="inline-flex items-center gap-2.5 py-1 font-medium"
|
||||
>
|
||||
{nav.title}
|
||||
</Link>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
type ButtonHTMLAttributes,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
@@ -22,17 +21,6 @@ import {
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import type { SidebarComponents } from '@/layouts/docs';
|
||||
import {
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarFolderLink,
|
||||
SidebarFolderTrigger,
|
||||
SidebarItem,
|
||||
SidebarSeparator,
|
||||
} from '@/layouts/docs/sidebar';
|
||||
import { useTreeContext, useTreePath } from '@/contexts/tree';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
|
||||
const itemVariants = cva(
|
||||
@@ -55,7 +43,9 @@ export function LinksMenu({ items, ...props }: LinksMenuProps) {
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger {...props} />
|
||||
<PopoverContent className="flex flex-col p-1">
|
||||
{items?.map((item, i) => <MenuItem key={i} item={item} />)}
|
||||
{items.map((item, i) => (
|
||||
<MenuItem key={i} item={item} />
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
@@ -114,94 +104,3 @@ export function MenuItem({ item, ...props }: MenuItemProps) {
|
||||
</BaseLinkItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarItems(props: {
|
||||
components?: Partial<SidebarComponents>;
|
||||
}) {
|
||||
const { root } = useTreeContext();
|
||||
|
||||
return (
|
||||
<div className="px-2 py-4 md:px-3">
|
||||
{renderSidebarList(root.children, 1, props?.components ?? {})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PageTreeFolder({
|
||||
item,
|
||||
children,
|
||||
level,
|
||||
}: {
|
||||
item: PageTree.Folder;
|
||||
level: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const path = useTreePath();
|
||||
|
||||
return (
|
||||
<SidebarFolder
|
||||
defaultOpen={item.defaultOpen || path.includes(item)}
|
||||
level={level + 1}
|
||||
>
|
||||
{children}
|
||||
</SidebarFolder>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSidebarList(
|
||||
items: PageTree.Node[],
|
||||
level: number,
|
||||
customComps: Partial<SidebarComponents>,
|
||||
): ReactNode[] {
|
||||
const { Separator, Item, Folder } = customComps;
|
||||
|
||||
return items.map((item, i) => {
|
||||
const id = `${item.type}_${i.toString()}`;
|
||||
|
||||
switch (item.type) {
|
||||
case 'separator':
|
||||
return Separator ? (
|
||||
<Separator key={id} item={item} />
|
||||
) : (
|
||||
<SidebarSeparator key={id}>{item.name}</SidebarSeparator>
|
||||
);
|
||||
case 'folder':
|
||||
return Folder ? (
|
||||
<Folder key={id} item={item} level={level + 1} />
|
||||
) : (
|
||||
<PageTreeFolder key={id} item={item} level={level + 1}>
|
||||
{item.index ? (
|
||||
<SidebarFolderLink
|
||||
href={item.index.url}
|
||||
external={item.index.external}
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</SidebarFolderLink>
|
||||
) : (
|
||||
<SidebarFolderTrigger>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</SidebarFolderTrigger>
|
||||
)}
|
||||
<SidebarFolderContent>
|
||||
{renderSidebarList(item.children, level + 1, customComps)}
|
||||
</SidebarFolderContent>
|
||||
</PageTreeFolder>
|
||||
);
|
||||
default:
|
||||
return Item ? (
|
||||
<Item key={item.url} item={item} />
|
||||
) : (
|
||||
<SidebarItem
|
||||
key={item.url}
|
||||
href={item.url}
|
||||
external={item.external}
|
||||
icon={item.icon}
|
||||
>
|
||||
{item.name}
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { type ReactNode, type HTMLAttributes, Fragment, type FC } from 'react';
|
||||
import { type ReactNode, type HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Languages, MoreHorizontal } from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
@@ -11,13 +11,7 @@ import {
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarCollapseTrigger,
|
||||
type SidebarProps,
|
||||
SidebarViewport,
|
||||
SidebarItem,
|
||||
SidebarFolder,
|
||||
SidebarFolderLink,
|
||||
SidebarFolderTrigger,
|
||||
SidebarFolderContent,
|
||||
} from '@/layouts/docs/sidebar';
|
||||
import { replaceOrDefault, type SharedNavProps } from '@/layouts/shared';
|
||||
import {
|
||||
@@ -26,13 +20,13 @@ import {
|
||||
BaseLinkItem,
|
||||
} from '@/layouts/links';
|
||||
import { getSidebarTabs, type TabOptions } from '@/utils/get-sidebar-tabs';
|
||||
import { type Option, RootToggle } from '@/components/layout/root-toggle';
|
||||
import { RootToggle } from '@/components/layout/root-toggle';
|
||||
import { type BaseLayoutProps, getLinks } from './shared';
|
||||
import {
|
||||
LanguageToggle,
|
||||
LanguageToggleText,
|
||||
} from '@/components/layout/language-toggle';
|
||||
import { SidebarItems, LinksMenu } from '@/layouts/docs.client';
|
||||
import { LinksMenu } from '@/layouts/docs.client';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { NavProvider, Title } from '@/components/layout/nav';
|
||||
import { ThemeToggle } from '@/components/layout/theme-toggle';
|
||||
@@ -42,35 +36,12 @@ import {
|
||||
SearchToggle,
|
||||
} from '@/components/layout/search-toggle';
|
||||
import { SearchOnly } from '@/contexts/search';
|
||||
|
||||
interface SidebarOptions extends SidebarProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
|
||||
collapsible?: boolean;
|
||||
components?: Partial<SidebarComponents>;
|
||||
|
||||
/**
|
||||
* Root Toggle options
|
||||
*/
|
||||
tabs?: Option[] | TabOptions | false;
|
||||
|
||||
banner?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* Hide search trigger
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
hideSearch?: boolean;
|
||||
}
|
||||
|
||||
export interface SidebarComponents {
|
||||
Item: FC<{ item: PageTree.Item }>;
|
||||
Folder: FC<{ item: PageTree.Folder; level: number }>;
|
||||
Separator: FC<{ item: PageTree.Separator }>;
|
||||
}
|
||||
import {
|
||||
getSidebarTabsFromOptions,
|
||||
SidebarLinkItem,
|
||||
type SidebarOptions,
|
||||
} from '@/layouts/docs/shared';
|
||||
import { SidebarItems } from '@/layouts/docs/sidebar-items';
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
@@ -104,14 +75,7 @@ export function DocsLayout({
|
||||
const Aside = collapsible ? CollapsibleSidebar : Sidebar;
|
||||
if (props.tree === undefined) notFound();
|
||||
|
||||
let tabs: Option[] = [];
|
||||
if (Array.isArray(tabOptions)) {
|
||||
tabs = tabOptions;
|
||||
} else if (typeof tabOptions === 'object') {
|
||||
tabs = getSidebarTabs(props.tree, tabOptions);
|
||||
} else if (tabOptions !== false) {
|
||||
tabs = getSidebarTabs(props.tree);
|
||||
}
|
||||
const tabs = getSidebarTabsFromOptions(tabOptions, props.tree) ?? [];
|
||||
|
||||
return (
|
||||
<TreeContextProvider tree={props.tree}>
|
||||
@@ -126,7 +90,7 @@ export function DocsLayout({
|
||||
<SearchOnly>
|
||||
<SearchToggle />
|
||||
</SearchOnly>
|
||||
<NavbarSidebarTrigger />
|
||||
<NavbarSidebarTrigger className="-me-2 md:hidden" />
|
||||
</Navbar>,
|
||||
nav,
|
||||
)}
|
||||
@@ -134,13 +98,16 @@ export function DocsLayout({
|
||||
id="nd-docs-layout"
|
||||
{...props.containerProps}
|
||||
className={cn(
|
||||
'flex flex-1 flex-row',
|
||||
'flex flex-1 flex-row md:[--fd-sidebar-width:260px] lg:[--fd-toc-width:240px] xl:[--fd-toc-width:260px]',
|
||||
!navReplace && navEnabled
|
||||
? '[--fd-nav-height:3.5rem] md:[--fd-nav-height:0px]'
|
||||
: null,
|
||||
props.containerProps?.className,
|
||||
)}
|
||||
>
|
||||
{collapsible ? (
|
||||
<SidebarCollapseTrigger className="fixed bottom-3 start-2 z-40 transition-opacity data-[collapsed=false]:pointer-events-none data-[collapsed=false]:opacity-0 max-md:hidden" />
|
||||
) : null}
|
||||
{replaceOrDefault(
|
||||
{ enabled: sidebarEnabled, component: sidebarReplace },
|
||||
<Aside {...sidebar}>
|
||||
@@ -159,10 +126,12 @@ export function DocsLayout({
|
||||
{links
|
||||
.filter((v) => v.type !== 'icon')
|
||||
.map((item, i) => (
|
||||
<Fragment key={i}>{renderLinkItem(item)}</Fragment>
|
||||
<SidebarLinkItem key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
<SidebarItems components={sidebarComponents} />
|
||||
<div className="px-2 py-4 md:px-3">
|
||||
<SidebarItems components={sidebarComponents} />
|
||||
</div>
|
||||
</SidebarViewport>
|
||||
<SidebarFooter>
|
||||
<SidebarFooterItems
|
||||
@@ -186,60 +155,10 @@ export function DocsLayout({
|
||||
);
|
||||
}
|
||||
|
||||
function renderLinkItem(item: LinkItemType): ReactNode {
|
||||
if (!item.type || item.type === 'main' || item.type === 'icon')
|
||||
return (
|
||||
<SidebarItem href={item.url} icon={item.icon} external={item.external}>
|
||||
{item.text}
|
||||
</SidebarItem>
|
||||
);
|
||||
|
||||
if (item.type === 'menu')
|
||||
return (
|
||||
<SidebarFolder level={1}>
|
||||
{item.url ? (
|
||||
<SidebarFolderLink href={item.url}>
|
||||
{item.icon}
|
||||
{item.text}
|
||||
</SidebarFolderLink>
|
||||
) : (
|
||||
<SidebarFolderTrigger>
|
||||
{item.icon}
|
||||
{item.text}
|
||||
</SidebarFolderTrigger>
|
||||
)}
|
||||
<SidebarFolderContent>
|
||||
{item.items.map((child, i) => (
|
||||
<Fragment key={i}>{renderLinkItem(child)}</Fragment>
|
||||
))}
|
||||
</SidebarFolderContent>
|
||||
</SidebarFolder>
|
||||
);
|
||||
|
||||
if (item.type === 'button') {
|
||||
return (
|
||||
<BaseLinkItem
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
className: 'gap-1.5 [&_svg]:size-4',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
{item.text}
|
||||
</BaseLinkItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === 'custom') return item.children;
|
||||
}
|
||||
|
||||
function SidebarHeaderItems({
|
||||
links,
|
||||
...props
|
||||
}: SharedNavProps & { links: LinkItemType[] }): ReactNode {
|
||||
}: SharedNavProps & { links: LinkItemType[] }) {
|
||||
const isEmpty = !props.title && !props.children && links.length === 0;
|
||||
if (isEmpty) return null;
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
'use client';
|
||||
import { useSidebar } from '@/contexts/sidebar';
|
||||
import { type HTMLAttributes, useContext } from 'react';
|
||||
import {
|
||||
type ButtonHTMLAttributes,
|
||||
type HTMLAttributes,
|
||||
useContext,
|
||||
} from 'react';
|
||||
import { NavContext } from '@/components/layout/nav';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { SidebarTrigger } from 'fumadocs-core/sidebar';
|
||||
@@ -16,7 +20,7 @@ export function Navbar(props: HTMLAttributes<HTMLElement>) {
|
||||
id="nd-subnav"
|
||||
{...props}
|
||||
className={cn(
|
||||
'sticky top-[var(--fd-banner-height)] z-40 flex flex-row items-center border-b border-fd-foreground/10 px-4 transition-colors',
|
||||
'sticky top-[var(--fd-banner-height)] z-30 flex flex-row items-center border-b border-fd-foreground/10 px-4 transition-colors',
|
||||
(!isTransparent || open) && 'bg-fd-background/80 backdrop-blur-lg',
|
||||
props.className,
|
||||
)}
|
||||
@@ -26,17 +30,20 @@ export function Navbar(props: HTMLAttributes<HTMLElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function NavbarSidebarTrigger() {
|
||||
export function NavbarSidebarTrigger(
|
||||
props: ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
) {
|
||||
const { open } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon',
|
||||
className: '-me-2 md:hidden',
|
||||
}),
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{open ? <X /> : <Menu />}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { BaseLinkItem, type LinkItemType } from '@/layouts/links';
|
||||
import {
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarFolderLink,
|
||||
SidebarFolderTrigger,
|
||||
SidebarItem,
|
||||
type SidebarProps,
|
||||
} from '@/layouts/docs/sidebar';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { getSidebarTabs, type TabOptions } from '@/utils/get-sidebar-tabs';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import type { Option } from '@/components/layout/root-toggle';
|
||||
|
||||
export interface SidebarOptions extends SidebarProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
|
||||
collapsible?: boolean;
|
||||
components?: Partial<SidebarComponents>;
|
||||
|
||||
/**
|
||||
* Root Toggle options
|
||||
*/
|
||||
tabs?: Option[] | TabOptions | false;
|
||||
|
||||
banner?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* Hide search trigger
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
hideSearch?: boolean;
|
||||
}
|
||||
|
||||
export interface SidebarComponents {
|
||||
Item: FC<{ item: PageTree.Item }>;
|
||||
Folder: FC<{ item: PageTree.Folder; level: number }>;
|
||||
Separator: FC<{ item: PageTree.Separator }>;
|
||||
}
|
||||
|
||||
export function SidebarLinkItem({ item }: { item: LinkItemType }) {
|
||||
if (item.type === 'menu')
|
||||
return (
|
||||
<SidebarFolder level={1}>
|
||||
{item.url ? (
|
||||
<SidebarFolderLink href={item.url}>
|
||||
{item.icon}
|
||||
{item.text}
|
||||
</SidebarFolderLink>
|
||||
) : (
|
||||
<SidebarFolderTrigger>
|
||||
{item.icon}
|
||||
{item.text}
|
||||
</SidebarFolderTrigger>
|
||||
)}
|
||||
<SidebarFolderContent>
|
||||
{item.items.map((child, i) => (
|
||||
<SidebarLinkItem key={i} item={child} />
|
||||
))}
|
||||
</SidebarFolderContent>
|
||||
</SidebarFolder>
|
||||
);
|
||||
|
||||
if (item.type === 'button') {
|
||||
return (
|
||||
<BaseLinkItem
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
className: 'gap-1.5 [&_svg]:size-4',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
{item.text}
|
||||
</BaseLinkItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === 'custom') return item.children;
|
||||
|
||||
return (
|
||||
<SidebarItem href={item.url} icon={item.icon} external={item.external}>
|
||||
{item.text}
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function getSidebarTabsFromOptions(
|
||||
options: SidebarOptions['tabs'],
|
||||
tree: PageTree.Root,
|
||||
) {
|
||||
if (Array.isArray(options)) {
|
||||
return options;
|
||||
} else if (typeof options === 'object') {
|
||||
return getSidebarTabs(tree, options);
|
||||
} else if (options !== false) {
|
||||
return getSidebarTabs(tree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { useTreeContext, useTreePath } from '@/contexts/tree';
|
||||
import {
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarFolderLink,
|
||||
SidebarFolderTrigger,
|
||||
SidebarItem,
|
||||
SidebarSeparator,
|
||||
} from '@/layouts/docs/sidebar';
|
||||
import type { SidebarComponents } from '@/layouts/docs/shared';
|
||||
|
||||
function PageTreeFolder({
|
||||
item,
|
||||
children,
|
||||
level,
|
||||
}: {
|
||||
item: PageTree.Folder;
|
||||
level: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const path = useTreePath();
|
||||
|
||||
return (
|
||||
<SidebarFolder
|
||||
defaultOpen={item.defaultOpen || path.includes(item)}
|
||||
level={level + 1}
|
||||
>
|
||||
{item.index ? (
|
||||
<SidebarFolderLink href={item.index.url} external={item.index.external}>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</SidebarFolderLink>
|
||||
) : (
|
||||
<SidebarFolderTrigger>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</SidebarFolderTrigger>
|
||||
)}
|
||||
<SidebarFolderContent>{children}</SidebarFolderContent>
|
||||
</SidebarFolder>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSidebarList(
|
||||
items: PageTree.Node[],
|
||||
level: number,
|
||||
customComps: Partial<SidebarComponents>,
|
||||
): ReactNode[] {
|
||||
const { Separator, Item, Folder } = customComps;
|
||||
|
||||
return items.map((item, i) => {
|
||||
const id = `${item.type}_${i.toString()}`;
|
||||
|
||||
switch (item.type) {
|
||||
case 'separator':
|
||||
return Separator ? (
|
||||
<Separator key={id} item={item} />
|
||||
) : (
|
||||
<SidebarSeparator key={id}>{item.name}</SidebarSeparator>
|
||||
);
|
||||
case 'folder':
|
||||
return Folder ? (
|
||||
<Folder key={id} item={item} level={level + 1} />
|
||||
) : (
|
||||
<PageTreeFolder key={id} item={item} level={level + 1}>
|
||||
{renderSidebarList(item.children, level + 1, customComps)}
|
||||
</PageTreeFolder>
|
||||
);
|
||||
default:
|
||||
return Item ? (
|
||||
<Item key={item.url} item={item} />
|
||||
) : (
|
||||
<SidebarItem
|
||||
key={item.url}
|
||||
href={item.url}
|
||||
external={item.external}
|
||||
icon={item.icon}
|
||||
>
|
||||
{item.name}
|
||||
</SidebarItem>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render sidebar items from page tree
|
||||
*/
|
||||
export function SidebarItems(props: {
|
||||
components?: Partial<SidebarComponents>;
|
||||
}) {
|
||||
const { root } = useTreeContext();
|
||||
|
||||
return useMemo(
|
||||
() => renderSidebarList(root.children, 1, props.components ?? {}),
|
||||
[root, props.components],
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -111,32 +111,25 @@ export function CollapsibleSidebar(props: SidebarProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarCollapseTrigger
|
||||
className={cn(
|
||||
'fixed bottom-3 start-2 z-20 transition-opacity max-md:hidden',
|
||||
(!collapsed || hover) && 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
<Sidebar
|
||||
{...props}
|
||||
onPointerEnter={onEnter}
|
||||
onPointerLeave={onLeave}
|
||||
className={cn(
|
||||
'transition-[flex,margin,opacity,transform]',
|
||||
collapsed &&
|
||||
'md:-me-[var(--fd-sidebar-offset)] md:flex-initial md:translate-x-[calc(var(--fd-sidebar-offset)*-1)] rtl:md:translate-x-[var(--fd-sidebar-offset)]',
|
||||
collapsed && hover && 'md:translate-x-0',
|
||||
collapsed && !hover && 'md:z-10 md:opacity-0',
|
||||
props.className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--fd-sidebar-offset': 'calc(var(--fd-sidebar-width) - 30px)',
|
||||
} as object
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<Sidebar
|
||||
{...props}
|
||||
onPointerEnter={onEnter}
|
||||
onPointerLeave={onLeave}
|
||||
data-collapsed={collapsed}
|
||||
className={cn(
|
||||
'transition-[flex,margin,opacity,transform]',
|
||||
collapsed &&
|
||||
'md:-me-[var(--fd-sidebar-width)] md:flex-initial md:translate-x-[calc(var(--fd-sidebar-offset)*-1)] rtl:md:translate-x-[var(--fd-sidebar-offset)]',
|
||||
collapsed && hover && 'z-50 md:translate-x-0',
|
||||
collapsed && !hover && 'md:opacity-0',
|
||||
props.className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--fd-sidebar-offset': 'calc(var(--fd-sidebar-width) - 20px)',
|
||||
} as object
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,7 +168,7 @@ export function Sidebar({
|
||||
<div
|
||||
{...inner}
|
||||
className={cn(
|
||||
'flex size-full flex-col pt-2 md:ms-auto md:w-[var(--fd-sidebar-width)] md:border-e md:pt-4',
|
||||
'flex size-full max-w-full flex-col pt-2 md:ms-auto md:w-[var(--fd-sidebar-width)] md:border-e md:pt-4',
|
||||
inner?.className,
|
||||
)}
|
||||
>
|
||||
@@ -319,7 +312,7 @@ export function SidebarFolderLink(props: LinkProps) {
|
||||
const active =
|
||||
props.href !== undefined && isActive(props.href, pathname, false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
setOpen(true);
|
||||
}
|
||||
@@ -368,12 +361,13 @@ export function SidebarFolderContent(props: CollapsibleContentProps) {
|
||||
export function SidebarCollapseTrigger(
|
||||
props: ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
) {
|
||||
const { setCollapsed } = useSidebar();
|
||||
const { collapsed, setCollapsed } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Collapse Sidebar"
|
||||
data-collapsed={collapsed}
|
||||
{...props}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { LinkItemType } from '@/layouts/links';
|
||||
import type { NavProviderProps, TitleProps } from '@/components/layout/nav';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
|
||||
interface NavOptions extends SharedNavProps {
|
||||
export interface NavOptions extends SharedNavProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
}
|
||||
@@ -61,6 +61,7 @@ export function getLinks(
|
||||
type: 'icon',
|
||||
url: githubUrl,
|
||||
text: 'Github',
|
||||
label: 'GitHub',
|
||||
icon: (
|
||||
<svg role="img" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Fragment,
|
||||
type HTMLAttributes,
|
||||
useContext,
|
||||
useEffect,
|
||||
@@ -12,11 +13,15 @@ import Link from 'next/link';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useI18n } from './contexts/i18n';
|
||||
import { useTreeContext } from './contexts/tree';
|
||||
import { useTreeContext, useTreePath } from './contexts/tree';
|
||||
import { useSidebar } from '@/contexts/sidebar';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { NavContext } from '@/components/layout/nav';
|
||||
import {
|
||||
type BreadcrumbOptions,
|
||||
getBreadcrumbItemsFromPath,
|
||||
} from 'fumadocs-core/breadcrumb';
|
||||
|
||||
export function PageContainer(props: HTMLAttributes<HTMLDivElement>) {
|
||||
const { collapsed } = useSidebar();
|
||||
@@ -174,3 +179,48 @@ export function Footer({ items }: FooterProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface BreadcrumbProps extends BreadcrumbOptions {
|
||||
/**
|
||||
* Show the full path to the current page
|
||||
*
|
||||
* @defaultValue false
|
||||
* @deprecated use `includePage` instead
|
||||
*/
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export function Breadcrumb(options: BreadcrumbProps) {
|
||||
const path = useTreePath();
|
||||
const { root } = useTreeContext();
|
||||
const items = useMemo(() => {
|
||||
return getBreadcrumbItemsFromPath(root, path, {
|
||||
includePage: options.includePage ?? false,
|
||||
...options,
|
||||
});
|
||||
}, [options, path, root]);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="-mb-3 flex flex-row items-center gap-1 text-sm font-medium text-fd-muted-foreground">
|
||||
{items.map((item, i) => (
|
||||
<Fragment key={i}>
|
||||
{i !== 0 && (
|
||||
<ChevronRight className="size-4 shrink-0 rtl:rotate-180" />
|
||||
)}
|
||||
{item.url ? (
|
||||
<Link
|
||||
href={item.url}
|
||||
className="truncate hover:text-fd-accent-foreground"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="truncate">{item.name}</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { LoaderConfig, LoaderOutput, Page } from 'fumadocs-core/source';
|
||||
import { type AnchorProviderProps, AnchorProvider } from 'fumadocs-core/toc';
|
||||
import { Card, Cards } from '@/components/card';
|
||||
@@ -17,11 +16,9 @@ import {
|
||||
LastUpdate,
|
||||
PageContainer,
|
||||
PageHeader,
|
||||
} from './page.client';
|
||||
import {
|
||||
Breadcrumb,
|
||||
type BreadcrumbProps,
|
||||
} from '@/components/layout/breadcrumb';
|
||||
} from './page.client';
|
||||
import { Toc, TOCItems, type TOCProps } from '@/components/layout/toc';
|
||||
import {
|
||||
TocPopoverTrigger,
|
||||
@@ -31,8 +28,7 @@ import {
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Edit, Text } from 'lucide-react';
|
||||
import { I18nLabel } from '@/contexts/i18n';
|
||||
|
||||
const ClerkTOCItems = dynamic(() => import('@/components/layout/toc-clerk'));
|
||||
import ClerkTOCItems from '@/components/layout/toc-clerk';
|
||||
|
||||
type TableOfContentOptions = Omit<TOCProps, 'items' | 'children'> &
|
||||
Pick<AnchorProviderProps, 'single'> & {
|
||||
@@ -175,7 +171,7 @@ export function DocsPage({
|
||||
{replaceOrDefault(
|
||||
{ enabled: tocEnabled, component: tocReplace },
|
||||
<Toc>
|
||||
<div className="flex h-full w-[var(--fd-toc-width)] max-w-full flex-col gap-3">
|
||||
<div className="flex h-full w-[var(--fd-toc-width)] max-w-full flex-col gap-3 pe-2">
|
||||
{tocOptions.header}
|
||||
<h3 className="-ms-0.5 inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground">
|
||||
<Text className="size-4" />
|
||||
|
||||
@@ -142,19 +142,6 @@ export const docsUi = plugin.withOptions<DocsUIOptions>(
|
||||
'background-color': `theme('colors.fd-background')`,
|
||||
color: `theme('colors.fd-foreground')`,
|
||||
},
|
||||
'@screen md': {
|
||||
':root': {
|
||||
'--fd-sidebar-width': '260px',
|
||||
},
|
||||
},
|
||||
'@screen lg': {
|
||||
':root': {
|
||||
'--fd-toc-width': '240px',
|
||||
},
|
||||
},
|
||||
'@screen xl': {
|
||||
':root': { '--fd-toc-width': '260px' },
|
||||
},
|
||||
});
|
||||
|
||||
if (css) addComponents(css);
|
||||
|
||||
Reference in New Issue
Block a user