mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
UI: Expose --fd-tocnav-height CSS variable
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Expose `--fd-tocnav-height` CSS variable
|
||||
@@ -66,13 +66,13 @@ export function API({ children, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-col gap-x-6 gap-y-4 max-xl:[--fd-toc-height:46px] max-md:[--fd-toc-height:36px] xl:flex-row xl:items-start',
|
||||
'flex flex-col gap-x-6 gap-y-4 xl:flex-row xl:items-start',
|
||||
props.className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--fd-api-info-top':
|
||||
'calc(var(--fd-nav-height) + var(--fd-banner-height) + var(--fd-toc-height, 0.5rem))',
|
||||
'calc(var(--fd-nav-height) + var(--fd-banner-height) + var(--fd-tocnav-height, 0px))',
|
||||
...props.style,
|
||||
} as object
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
PopoverTriggerProps,
|
||||
} from '@radix-ui/react-popover';
|
||||
import { ChevronRight, Text } from 'lucide-react';
|
||||
import { usePageStyles } from '@/contexts/layout';
|
||||
|
||||
export interface TOCProps {
|
||||
/**
|
||||
@@ -32,12 +33,15 @@ export interface TOCProps {
|
||||
}
|
||||
|
||||
export function Toc(props: HTMLAttributes<HTMLDivElement>) {
|
||||
const { toc } = usePageStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
id="nd-toc"
|
||||
{...props}
|
||||
data-toc=""
|
||||
className={cn(
|
||||
'sticky top-fd-layout-top h-[var(--fd-toc-height)] flex-1 pb-2 pt-12',
|
||||
'sticky top-fd-layout-top h-[var(--fd-toc-height)] pb-2 pt-12',
|
||||
toc,
|
||||
props.className,
|
||||
)}
|
||||
style={
|
||||
@@ -48,7 +52,9 @@ export function Toc(props: HTMLAttributes<HTMLDivElement>) {
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
<div className="flex h-full w-[var(--fd-toc-width)] max-w-full flex-col gap-3 pe-2">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const contextsMap = {
|
||||
'../contexts/search.tsx': 'fumadocs-ui/provider',
|
||||
'../contexts/tree.tsx': 'fumadocs-ui/provider',
|
||||
'../contexts/i18n.tsx': 'fumadocs-ui/provider',
|
||||
'../contexts/layout.tsx': 'fumadocs-ui/provider',
|
||||
};
|
||||
|
||||
export const registry: Registry = {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
import { createContext, type ReactNode, useContext } from 'react';
|
||||
|
||||
export interface PageStyles {
|
||||
tocNav?: string;
|
||||
toc?: string;
|
||||
page?: string;
|
||||
article?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* applied styles to different layout components in `Page` from layouts
|
||||
*/
|
||||
const StylesContext = createContext<PageStyles>({
|
||||
tocNav: 'xl:hidden',
|
||||
toc: 'max-xl:hidden',
|
||||
});
|
||||
|
||||
export function usePageStyles() {
|
||||
return useContext(StylesContext);
|
||||
}
|
||||
|
||||
export function StylesProvider({
|
||||
children,
|
||||
...value
|
||||
}: PageStyles & { children: ReactNode }) {
|
||||
return (
|
||||
<StylesContext.Provider value={value}>{children}</StylesContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
useState,
|
||||
useMemo,
|
||||
useRef,
|
||||
type MutableRefObject,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { SidebarProvider as BaseProvider } from 'fumadocs-core/sidebar';
|
||||
@@ -20,7 +20,7 @@ interface SidebarContext {
|
||||
/**
|
||||
* When set to false, don't close the sidebar when navigate to another page
|
||||
*/
|
||||
closeOnRedirect: MutableRefObject<boolean>;
|
||||
closeOnRedirect: RefObject<boolean>;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContext | undefined>(undefined);
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
'use client';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
type ReactNode,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { createContext, useContext, type ReactNode, useMemo } from 'react';
|
||||
import { searchPath } from 'fumadocs-core/breadcrumb';
|
||||
|
||||
interface TreeContextType {
|
||||
root: PageTree.Root | PageTree.Folder;
|
||||
}
|
||||
|
||||
const TreeContext = createContext<TreeContextType | undefined>(undefined);
|
||||
const TreeContext = createContext<TreeContextType | null>(null);
|
||||
const PathContext = createContext<PageTree.Node[]>([]);
|
||||
|
||||
export function TreeContextProvider({
|
||||
@@ -32,8 +26,6 @@ export function TreeContextProvider({
|
||||
|
||||
const root = (path.findLast((item) => item.type === 'folder' && item.root) ??
|
||||
tree) as PageTree.Root;
|
||||
const pathnameRef = useRef(pathname);
|
||||
pathnameRef.current = pathname;
|
||||
|
||||
return (
|
||||
<TreeContext.Provider value={useMemo(() => ({ root }), [root])}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { ChevronDown, Menu, X } from 'lucide-react';
|
||||
import {
|
||||
type ButtonHTMLAttributes,
|
||||
type HTMLAttributes,
|
||||
@@ -23,23 +23,8 @@ import {
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { useSidebar } from '@/contexts/sidebar';
|
||||
|
||||
export function LayoutBody(props: HTMLAttributes<HTMLElement>) {
|
||||
const { collapsed } = useSidebar();
|
||||
|
||||
return (
|
||||
<main
|
||||
{...props}
|
||||
className={cn(
|
||||
!collapsed &&
|
||||
'[&_#nd-page]:max-w-[calc(min(100vw,var(--fd-layout-width))-var(--fd-sidebar-width)-var(--fd-toc-width))]',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
import { useNav } from '@/components/layout/nav';
|
||||
import { SidebarTrigger } from 'fumadocs-core/sidebar';
|
||||
|
||||
const itemVariants = cva(
|
||||
'flex flex-row items-center gap-2 rounded-md px-3 py-2.5 text-fd-muted-foreground transition-colors duration-100 [overflow-wrap:anywhere] hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none md:px-2 md:py-1.5 [&_svg]:size-4',
|
||||
@@ -69,11 +54,51 @@ export function LinksMenu({ items, ...props }: LinksMenuProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Navbar(props: HTMLAttributes<HTMLElement>) {
|
||||
const { open } = useSidebar();
|
||||
const { isTransparent } = useNav();
|
||||
|
||||
return (
|
||||
<header
|
||||
id="nd-subnav"
|
||||
{...props}
|
||||
className={cn(
|
||||
'sticky top-[var(--fd-banner-height)] z-30 flex h-14 flex-row items-center border-b border-fd-foreground/10 px-4 backdrop-blur-lg transition-colors',
|
||||
(!isTransparent || open) && 'bg-fd-background/80',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavbarSidebarTrigger(
|
||||
props: ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
) {
|
||||
const { open } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon',
|
||||
}),
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{open ? <X /> : <Menu />}
|
||||
</SidebarTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
interface MenuItemProps extends HTMLAttributes<HTMLElement> {
|
||||
item: LinkItemType;
|
||||
}
|
||||
|
||||
export function MenuItem({ item, ...props }: MenuItemProps) {
|
||||
function MenuItem({ item, ...props }: MenuItemProps) {
|
||||
if (item.type === 'custom')
|
||||
return (
|
||||
<div {...props} className={cn('grid', props.className)}>
|
||||
|
||||
@@ -27,11 +27,10 @@ import {
|
||||
LanguageToggle,
|
||||
LanguageToggleText,
|
||||
} from '@/components/layout/language-toggle';
|
||||
import { LayoutBody, LinksMenu } from '@/layouts/docs.client';
|
||||
import { LinksMenu, Navbar, NavbarSidebarTrigger } from '@/layouts/docs.client';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { NavProvider, Title } from '@/components/layout/nav';
|
||||
import { ThemeToggle } from '@/components/layout/theme-toggle';
|
||||
import { Navbar, NavbarSidebarTrigger } from '@/layouts/docs/navbar';
|
||||
import {
|
||||
LargeSearchToggle,
|
||||
SearchToggle,
|
||||
@@ -39,9 +38,11 @@ import {
|
||||
import { SearchOnly } from '@/contexts/search';
|
||||
import {
|
||||
getSidebarTabsFromOptions,
|
||||
layoutVariables,
|
||||
SidebarLinkItem,
|
||||
type SidebarOptions,
|
||||
} from '@/layouts/docs/shared';
|
||||
import { type PageStyles, StylesProvider } from '@/contexts/layout';
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
@@ -76,13 +77,24 @@ export function DocsLayout({
|
||||
if (props.tree === undefined) notFound();
|
||||
|
||||
const tabs = getSidebarTabsFromOptions(tabOptions, props.tree) ?? [];
|
||||
const variables = cn(
|
||||
'[--fd-tocnav-height:36px] md:[--fd-sidebar-width:260px] xl:[--fd-toc-width:260px] xl:[--fd-tocnav-height:0px]',
|
||||
!navReplace && navEnabled
|
||||
? '[--fd-nav-height:3.5rem] md:[--fd-nav-height:0px]'
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const pageStyles: PageStyles = {
|
||||
tocNav: cn('xl:hidden'),
|
||||
toc: cn('max-xl:hidden'),
|
||||
};
|
||||
|
||||
return (
|
||||
<TreeContextProvider tree={props.tree}>
|
||||
<NavProvider transparentMode={transparentMode}>
|
||||
{replaceOrDefault(
|
||||
{ enabled: navEnabled, component: navReplace },
|
||||
<Navbar id="nd-subnav" className="h-14 md:hidden">
|
||||
<Navbar className="md:hidden">
|
||||
<Title url={nav.url} title={nav.title} />
|
||||
<div className="flex flex-1 flex-row items-center gap-1">
|
||||
{nav.children}
|
||||
@@ -94,16 +106,18 @@ export function DocsLayout({
|
||||
</Navbar>,
|
||||
nav,
|
||||
)}
|
||||
<LayoutBody
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
{...props.containerProps}
|
||||
className={cn(
|
||||
'flex flex-1 flex-row md:[--fd-sidebar-width:260px] xl:[--fd-toc-width:260px] [&_#nd-toc]:max-xl:hidden [&_#nd-tocnav]:xl:hidden',
|
||||
!navReplace && navEnabled
|
||||
? '[--fd-nav-height:3.5rem] md:[--fd-nav-height:0px]'
|
||||
: null,
|
||||
'flex flex-1 flex-row pe-[var(--fd-layout-offset)]',
|
||||
variables,
|
||||
props.containerProps?.className,
|
||||
)}
|
||||
style={{
|
||||
...layoutVariables,
|
||||
...props.containerProps?.style,
|
||||
}}
|
||||
>
|
||||
{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" />
|
||||
@@ -112,7 +126,10 @@ export function DocsLayout({
|
||||
{ enabled: sidebarEnabled, component: sidebarReplace },
|
||||
<Aside
|
||||
{...sidebar}
|
||||
className="md:flex-1 md:data-[collapsed=true]:flex-initial"
|
||||
className={cn(
|
||||
'md:ps-[var(--fd-layout-offset)]',
|
||||
sidebar.className,
|
||||
)}
|
||||
>
|
||||
<SidebarHeader>
|
||||
<SidebarHeaderItems {...nav} links={links} />
|
||||
@@ -151,8 +168,8 @@ export function DocsLayout({
|
||||
tabs,
|
||||
},
|
||||
)}
|
||||
{props.children}
|
||||
</LayoutBody>
|
||||
<StylesProvider {...pageStyles}>{props.children}</StylesProvider>
|
||||
</main>
|
||||
</NavProvider>
|
||||
</TreeContextProvider>
|
||||
);
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
'use client';
|
||||
import { useSidebar } from '@/contexts/sidebar';
|
||||
import { type ButtonHTMLAttributes, type HTMLAttributes } from 'react';
|
||||
import { useNav } from '@/components/layout/nav';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { SidebarTrigger } from 'fumadocs-core/sidebar';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
|
||||
export function Navbar(props: HTMLAttributes<HTMLElement>) {
|
||||
const { open } = useSidebar();
|
||||
const { isTransparent } = useNav();
|
||||
|
||||
return (
|
||||
<header
|
||||
{...props}
|
||||
className={cn(
|
||||
'sticky top-[var(--fd-banner-height)] z-30 flex flex-row items-center border-b border-fd-foreground/10 px-4 backdrop-blur-lg transition-colors',
|
||||
(!isTransparent || open) && 'bg-fd-background/80',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavbarSidebarTrigger(
|
||||
props: ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
) {
|
||||
const { open } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon',
|
||||
}),
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{open ? <X /> : <Menu />}
|
||||
</SidebarTrigger>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,10 @@ import { getSidebarTabs, type TabOptions } from '@/utils/get-sidebar-tabs';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import type { Option } from '@/components/layout/root-toggle';
|
||||
|
||||
export const layoutVariables = {
|
||||
'--fd-layout-offset': 'max(calc(50vw - var(--fd-layout-width) / 2), 0px)',
|
||||
};
|
||||
|
||||
export interface SidebarOptions extends SidebarProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
|
||||
+124
-119
@@ -35,30 +35,26 @@ import { MenuLinkItem } from '@/layouts/home/menu';
|
||||
|
||||
export type HomeLayoutProps = BaseLayoutProps & HTMLAttributes<HTMLElement>;
|
||||
|
||||
export function HomeLayout({
|
||||
nav: { transparentMode, enableSearch = true, ...nav } = {},
|
||||
links = [],
|
||||
githubUrl,
|
||||
i18n = false,
|
||||
disableThemeSwitch,
|
||||
...props
|
||||
}: HomeLayoutProps): ReactNode {
|
||||
export function HomeLayout(props: HomeLayoutProps) {
|
||||
const {
|
||||
nav,
|
||||
links,
|
||||
githubUrl,
|
||||
i18n: _i18n,
|
||||
disableThemeSwitch: _disableThemeSwitch,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const finalLinks = getLinks(links, githubUrl);
|
||||
const navItems = finalLinks.filter((item) =>
|
||||
['nav', 'all'].includes(item.on ?? 'all'),
|
||||
);
|
||||
const menuItems = finalLinks.filter((item) =>
|
||||
['menu', 'all'].includes(item.on ?? 'all'),
|
||||
);
|
||||
|
||||
return (
|
||||
<NavProvider transparentMode={transparentMode}>
|
||||
<NavProvider transparentMode={nav?.transparentMode}>
|
||||
<main
|
||||
id="nd-home-layout"
|
||||
{...props}
|
||||
{...rest}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col pt-[var(--fd-nav-height)] [--fd-nav-height:56px]',
|
||||
props.className,
|
||||
rest.className,
|
||||
)}
|
||||
>
|
||||
{replaceOrDefault(
|
||||
@@ -71,85 +67,10 @@ export function HomeLayout({
|
||||
maskImage: 'linear-gradient(to bottom,white,transparent)',
|
||||
}}
|
||||
/>
|
||||
<Navbar>
|
||||
<Title title={nav.title} url={nav.url} />
|
||||
{nav.children}
|
||||
<NavigationMenuList className="flex flex-row items-center gap-2 max-sm:hidden">
|
||||
{navItems
|
||||
.filter((item) => !isSecondary(item))
|
||||
.map((item, i) => (
|
||||
<NavbarLinkItem key={i} item={item} className="text-sm" />
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
<div className="flex flex-1 flex-row items-center justify-end lg:gap-1.5">
|
||||
{enableSearch ? (
|
||||
<SearchOnly>
|
||||
<SearchToggle className="lg:hidden" />
|
||||
<LargeSearchToggle className="w-full max-w-[240px] max-lg:hidden" />
|
||||
</SearchOnly>
|
||||
) : null}
|
||||
{!disableThemeSwitch ? (
|
||||
<ThemeToggle className="max-lg:hidden" />
|
||||
) : null}
|
||||
{i18n ? (
|
||||
<LanguageToggle className="-me-1.5 max-lg:hidden">
|
||||
<Languages className="size-5" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
{navItems.filter(isSecondary).map((item, i) => (
|
||||
<NavbarLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className="-me-1.5 list-none max-lg:hidden"
|
||||
/>
|
||||
))}
|
||||
<NavigationMenuItem className="list-none lg:hidden">
|
||||
<NavigationMenuTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon',
|
||||
color: 'ghost',
|
||||
}),
|
||||
'group -me-2',
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="size-3 transition-transform duration-300 group-data-[state=open]:rotate-180" />
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="flex flex-col p-4 sm:flex-row sm:items-center sm:justify-end">
|
||||
{menuItems
|
||||
.filter((item) => !isSecondary(item))
|
||||
.map((item, i) => (
|
||||
<MenuLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className="sm:hidden"
|
||||
/>
|
||||
))}
|
||||
<div className="-ms-1.5 flex flex-row items-center gap-1.5 max-sm:mt-2">
|
||||
{menuItems.filter(isSecondary).map((item, i) => (
|
||||
<MenuLinkItem key={i} item={item} className="-me-1.5" />
|
||||
))}
|
||||
<div role="separator" className="flex-1" />
|
||||
{i18n ? (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-5" />
|
||||
<LanguageToggleText />
|
||||
<ChevronDown className="size-3 text-fd-muted-foreground" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
{!disableThemeSwitch ? <ThemeToggle /> : null}
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</div>
|
||||
</Navbar>
|
||||
<Header finalLinks={finalLinks} {...props} />
|
||||
</>,
|
||||
{
|
||||
items: finalLinks,
|
||||
i18n,
|
||||
enableSearch,
|
||||
disableThemeSwitch,
|
||||
...nav,
|
||||
},
|
||||
)}
|
||||
{props.children}
|
||||
@@ -158,6 +79,91 @@ export function HomeLayout({
|
||||
);
|
||||
}
|
||||
|
||||
function Header({
|
||||
nav: { enableSearch = true, ...nav } = {},
|
||||
i18n = false,
|
||||
finalLinks,
|
||||
disableThemeSwitch,
|
||||
}: BaseLayoutProps & {
|
||||
finalLinks: LinkItemType[];
|
||||
}) {
|
||||
const navItems = finalLinks.filter((item) =>
|
||||
['nav', 'all'].includes(item.on ?? 'all'),
|
||||
);
|
||||
const menuItems = finalLinks.filter((item) =>
|
||||
['menu', 'all'].includes(item.on ?? 'all'),
|
||||
);
|
||||
|
||||
return (
|
||||
<Navbar>
|
||||
<Title title={nav.title} url={nav.url} />
|
||||
{nav.children}
|
||||
<NavigationMenuList className="flex flex-row items-center gap-2 max-sm:hidden">
|
||||
{navItems
|
||||
.filter((item) => !isSecondary(item))
|
||||
.map((item, i) => (
|
||||
<NavbarLinkItem key={i} item={item} className="text-sm" />
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
<div className="flex flex-1 flex-row items-center justify-end lg:gap-1.5">
|
||||
{enableSearch ? (
|
||||
<SearchOnly>
|
||||
<SearchToggle className="lg:hidden" />
|
||||
<LargeSearchToggle className="w-full max-w-[240px] max-lg:hidden" />
|
||||
</SearchOnly>
|
||||
) : null}
|
||||
{!disableThemeSwitch ? <ThemeToggle className="max-lg:hidden" /> : null}
|
||||
{i18n ? (
|
||||
<LanguageToggle className="-me-1.5 max-lg:hidden">
|
||||
<Languages className="size-5" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
{navItems.filter(isSecondary).map((item, i) => (
|
||||
<NavbarLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className="-me-1.5 list-none max-lg:hidden"
|
||||
/>
|
||||
))}
|
||||
<NavigationMenuItem className="list-none lg:hidden">
|
||||
<NavigationMenuTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon',
|
||||
color: 'ghost',
|
||||
}),
|
||||
'group -me-2',
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="size-3 transition-transform duration-300 group-data-[state=open]:rotate-180" />
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="flex flex-col p-4 sm:flex-row sm:items-center sm:justify-end">
|
||||
{menuItems
|
||||
.filter((item) => !isSecondary(item))
|
||||
.map((item, i) => (
|
||||
<MenuLinkItem key={i} item={item} className="sm:hidden" />
|
||||
))}
|
||||
<div className="-ms-1.5 flex flex-row items-center gap-1.5 max-sm:mt-2">
|
||||
{menuItems.filter(isSecondary).map((item, i) => (
|
||||
<MenuLinkItem key={i} item={item} className="-me-1.5" />
|
||||
))}
|
||||
<div role="separator" className="flex-1" />
|
||||
{i18n ? (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-5" />
|
||||
<LanguageToggleText />
|
||||
<ChevronDown className="size-3 text-fd-muted-foreground" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
{!disableThemeSwitch ? <ThemeToggle /> : null}
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</div>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
function NavbarLinkItem({
|
||||
item,
|
||||
...props
|
||||
@@ -168,37 +174,36 @@ function NavbarLinkItem({
|
||||
if (item.type === 'custom') return <div {...props}>{item.children}</div>;
|
||||
|
||||
if (item.type === 'menu') {
|
||||
const children = item.items.map((child, j) => {
|
||||
if (child.type === 'custom') return <div key={j}>{child.children}</div>;
|
||||
|
||||
const { banner, footer, ...rest } = child.menu ?? {};
|
||||
|
||||
return (
|
||||
<NavbarMenuItem key={j} href={child.url} {...rest}>
|
||||
{banner ??
|
||||
(child.icon ? (
|
||||
<div className="w-fit rounded-md border bg-fd-muted p-1 [&_svg]:size-4">
|
||||
{child.icon}
|
||||
</div>
|
||||
) : null)}
|
||||
<p className="-mb-1 text-sm font-medium">{child.text}</p>
|
||||
{child.description ? (
|
||||
<p className="text-[13px] text-fd-muted-foreground">
|
||||
{child.description}
|
||||
</p>
|
||||
) : null}
|
||||
{footer}
|
||||
</NavbarMenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<NavbarMenu>
|
||||
<NavbarMenuTrigger {...props}>
|
||||
{item.url ? <Link href={item.url}>{item.text}</Link> : item.text}
|
||||
</NavbarMenuTrigger>
|
||||
<NavbarMenuContent>
|
||||
{item.items.map((child, j) => {
|
||||
if (child.type === 'custom')
|
||||
return <div key={j}>{child.children}</div>;
|
||||
|
||||
const { banner, footer, ...rest } = child.menu ?? {};
|
||||
|
||||
return (
|
||||
<NavbarMenuItem key={j} href={child.url} {...rest}>
|
||||
{banner ??
|
||||
(child.icon ? (
|
||||
<div className="w-fit rounded-md border bg-fd-muted p-1 [&_svg]:size-4">
|
||||
{child.icon}
|
||||
</div>
|
||||
) : null)}
|
||||
<p className="-mb-1 text-sm font-medium">{child.text}</p>
|
||||
{child.description ? (
|
||||
<p className="text-[13px] text-fd-muted-foreground">
|
||||
{child.description}
|
||||
</p>
|
||||
) : null}
|
||||
{footer}
|
||||
</NavbarMenuItem>
|
||||
);
|
||||
})}
|
||||
</NavbarMenuContent>
|
||||
<NavbarMenuContent>{children}</NavbarMenuContent>
|
||||
</NavbarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,27 +7,7 @@ import { SidebarTrigger } from 'fumadocs-core/sidebar';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
|
||||
export function LayoutBody(props: HTMLAttributes<HTMLElement>) {
|
||||
return (
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
{...props}
|
||||
className={cn('flex w-full flex-1 flex-row', props.className)}
|
||||
style={
|
||||
{
|
||||
...props.style,
|
||||
'--fd-layout-offset':
|
||||
'max(calc(50vw - var(--fd-layout-width) / 2), 0px)',
|
||||
paddingInlineEnd: 'var(--fd-layout-offset)',
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubNavbar(props: HTMLAttributes<HTMLElement>) {
|
||||
export function Navbar(props: HTMLAttributes<HTMLElement>) {
|
||||
const { open, collapsed } = useSidebar();
|
||||
const { isTransparent } = useNav();
|
||||
|
||||
@@ -36,7 +16,7 @@ export function SubNavbar(props: HTMLAttributes<HTMLElement>) {
|
||||
id="nd-subnav"
|
||||
{...props}
|
||||
className={cn(
|
||||
'fixed inset-x-0 top-[var(--fd-banner-height)] z-10 h-14 backdrop-blur-lg transition-colors',
|
||||
'fixed inset-x-0 top-[var(--fd-banner-height)] z-10 h-14 pe-[var(--fd-layout-offset)] backdrop-blur-lg transition-colors',
|
||||
(!isTransparent || open) && 'bg-fd-background/80',
|
||||
props.className,
|
||||
)}
|
||||
@@ -45,7 +25,6 @@ export function SubNavbar(props: HTMLAttributes<HTMLElement>) {
|
||||
paddingInlineStart: collapsed
|
||||
? 'calc(var(--fd-layout-offset))'
|
||||
: 'calc(var(--fd-layout-offset) + var(--fd-sidebar-width))',
|
||||
paddingInlineEnd: 'var(--fd-layout-offset)',
|
||||
} as object
|
||||
}
|
||||
>
|
||||
|
||||
@@ -36,11 +36,13 @@ import {
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
getSidebarTabsFromOptions,
|
||||
layoutVariables,
|
||||
SidebarLinkItem,
|
||||
type SidebarOptions,
|
||||
} from '@/layouts/docs/shared';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { LayoutBody, SubNavbar, NavbarSidebarTrigger } from './notebook.client';
|
||||
import { Navbar, NavbarSidebarTrigger } from './notebook.client';
|
||||
import { type PageStyles, StylesProvider } from '@/contexts/layout';
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
@@ -68,16 +70,31 @@ export function DocsLayout({
|
||||
if (props.tree === undefined) notFound();
|
||||
|
||||
const tabs = getSidebarTabsFromOptions(tabOptions, props.tree) ?? [];
|
||||
const variables = cn(
|
||||
'[--fd-nav-height:3.5rem] [--fd-tocnav-height:36px] md:[--fd-sidebar-width:260px] xl:[--fd-toc-width:260px] xl:[--fd-tocnav-height:0px]',
|
||||
);
|
||||
|
||||
const pageStyles: PageStyles = {
|
||||
tocNav: cn('lg:px-4 xl:hidden'),
|
||||
toc: cn('max-xl:hidden'),
|
||||
page: cn('mt-[var(--fd-nav-height)]'),
|
||||
};
|
||||
|
||||
return (
|
||||
<TreeContextProvider tree={props.tree}>
|
||||
<NavProvider transparentMode={transparentMode}>
|
||||
<LayoutBody
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
{...props.containerProps}
|
||||
className={cn(
|
||||
'[--fd-nav-height:3.5rem] md:[--fd-sidebar-width:260px] lg:[--fd-toc-width:260px] [&_#nd-page]:mt-[var(--fd-nav-height)] [&_#nd-toc]:max-lg:hidden [&_#nd-tocnav]:lg:hidden',
|
||||
'flex w-full flex-1 flex-row pe-[var(--fd-layout-offset)]',
|
||||
variables,
|
||||
props.containerProps?.className,
|
||||
)}
|
||||
style={{
|
||||
...layoutVariables,
|
||||
...props.containerProps?.style,
|
||||
}}
|
||||
>
|
||||
<Aside
|
||||
{...sidebar}
|
||||
@@ -116,8 +133,8 @@ export function DocsLayout({
|
||||
i18n={i18n}
|
||||
sidebarCollapsible={sidebarCollapsible}
|
||||
/>
|
||||
{props.children}
|
||||
</LayoutBody>
|
||||
<StylesProvider {...pageStyles}>{props.children}</StylesProvider>
|
||||
</main>
|
||||
</NavProvider>
|
||||
</TreeContextProvider>
|
||||
);
|
||||
@@ -135,7 +152,7 @@ function DocsNavbar({
|
||||
links: LinkItemType[];
|
||||
}) {
|
||||
return (
|
||||
<SubNavbar>
|
||||
<Navbar>
|
||||
{sidebarCollapsible ? (
|
||||
<SidebarCollapseTrigger className="-ms-1.5 text-fd-muted-foreground data-[collapsed=false]:hidden max-md:hidden" />
|
||||
) : null}
|
||||
@@ -182,7 +199,7 @@ function DocsNavbar({
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
<ThemeToggle className="p-0 max-md:hidden" />
|
||||
</SubNavbar>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,18 +21,22 @@ import {
|
||||
type BreadcrumbOptions,
|
||||
getBreadcrumbItemsFromPath,
|
||||
} from 'fumadocs-core/breadcrumb';
|
||||
import { usePageStyles } from '@/contexts/layout';
|
||||
|
||||
export function PageHeader(props: HTMLAttributes<HTMLDivElement>) {
|
||||
export function TocNav(props: HTMLAttributes<HTMLDivElement>) {
|
||||
const { open } = useSidebar();
|
||||
const { tocNav } = usePageStyles();
|
||||
const { isTransparent } = useNav();
|
||||
|
||||
return (
|
||||
<header
|
||||
id="nd-tocnav"
|
||||
{...props}
|
||||
className={cn(
|
||||
'sticky top-fd-layout-top z-10 flex flex-row items-center border-b border-fd-foreground/10 text-sm backdrop-blur-md transition-colors',
|
||||
!isTransparent && 'bg-fd-background/80',
|
||||
open && 'opacity-0',
|
||||
tocNav,
|
||||
props.className,
|
||||
)}
|
||||
style={
|
||||
@@ -48,6 +52,37 @@ export function PageHeader(props: HTMLAttributes<HTMLDivElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PageBody(props: HTMLAttributes<HTMLDivElement>) {
|
||||
const { page } = usePageStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
id="nd-page"
|
||||
{...props}
|
||||
className={cn('flex w-full min-w-0 flex-col', page, props.className)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageArticle(props: HTMLAttributes<HTMLElement>) {
|
||||
const { article } = usePageStyles();
|
||||
|
||||
return (
|
||||
<article
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex w-full flex-1 flex-col gap-6 px-4 pt-8 md:pt-12 lg:px-8 xl:mx-auto',
|
||||
article,
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function LastUpdate(props: { date: Date }) {
|
||||
const { text } = useI18n();
|
||||
const [date, setDate] = useState('');
|
||||
|
||||
+31
-36
@@ -14,9 +14,11 @@ import {
|
||||
Footer,
|
||||
type FooterProps,
|
||||
LastUpdate,
|
||||
PageHeader,
|
||||
TocNav,
|
||||
Breadcrumb,
|
||||
type BreadcrumbProps,
|
||||
PageBody,
|
||||
PageArticle,
|
||||
} from './page.client';
|
||||
import {
|
||||
Toc,
|
||||
@@ -139,26 +141,21 @@ export function DocsPage({
|
||||
tocPopoverOptions.header !== undefined ||
|
||||
tocPopoverOptions.footer !== undefined;
|
||||
|
||||
const fullWidth = full && !tocEnabled;
|
||||
|
||||
return (
|
||||
<AnchorProvider toc={toc} single={tocOptions.single}>
|
||||
<div
|
||||
id="nd-page"
|
||||
<PageBody
|
||||
{...props.container}
|
||||
className={cn(
|
||||
'flex w-full min-w-0 flex-col md:transition-[max-width]',
|
||||
props.container?.className,
|
||||
)}
|
||||
className={cn(props.container?.className)}
|
||||
style={
|
||||
{
|
||||
'--fd-toc-width': fullWidth ? '0px' : undefined,
|
||||
'--fd-tocnav-height': !tocPopoverEnabled ? '0px' : undefined,
|
||||
...props.container?.style,
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{replaceOrDefault(
|
||||
{ enabled: tocPopoverEnabled, component: tocPopoverReplace },
|
||||
<PageHeader id="nd-tocnav">
|
||||
<TocNav>
|
||||
<TocPopover>
|
||||
<TocPopoverTrigger className="size-full" items={toc} />
|
||||
<TocPopoverContent>
|
||||
@@ -171,17 +168,16 @@ export function DocsPage({
|
||||
{tocPopoverOptions.footer}
|
||||
</TocPopoverContent>
|
||||
</TocPopover>
|
||||
</PageHeader>,
|
||||
</TocNav>,
|
||||
{
|
||||
items: toc,
|
||||
...tocPopoverOptions,
|
||||
},
|
||||
)}
|
||||
<article
|
||||
<PageArticle
|
||||
{...props.article}
|
||||
className={cn(
|
||||
'mx-auto flex w-full flex-1 flex-col gap-6 px-4 pt-8 max-xl:mx-0 md:pt-12 lg:px-8',
|
||||
fullWidth ? 'max-w-[1120px]' : 'max-w-[860px]',
|
||||
full || !tocEnabled ? 'max-w-[1120px]' : 'max-w-[860px]',
|
||||
props.article?.className,
|
||||
)}
|
||||
>
|
||||
@@ -206,30 +202,27 @@ export function DocsPage({
|
||||
props.footer,
|
||||
<Footer items={props.footer?.items} />,
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
</PageArticle>
|
||||
</PageBody>
|
||||
{replaceOrDefault(
|
||||
{ enabled: tocEnabled, component: tocReplace },
|
||||
<Toc id="nd-toc">
|
||||
<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" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
{tocOptions.style === 'clerk' ? (
|
||||
<ClerkTOCItems items={toc} />
|
||||
) : (
|
||||
<TOCItems items={toc} />
|
||||
)}
|
||||
{tocOptions.footer}
|
||||
</div>
|
||||
<Toc>
|
||||
{tocOptions.header}
|
||||
<h3 className="-ms-0.5 inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground">
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
{tocOptions.style === 'clerk' ? (
|
||||
<ClerkTOCItems items={toc} />
|
||||
) : (
|
||||
<TOCItems items={toc} />
|
||||
)}
|
||||
{tocOptions.footer}
|
||||
</Toc>,
|
||||
{
|
||||
items: toc,
|
||||
...tocOptions,
|
||||
},
|
||||
<div role="none" className="flex-1" />,
|
||||
)}
|
||||
</AnchorProvider>
|
||||
);
|
||||
@@ -343,13 +336,15 @@ export function DocsCategory({
|
||||
from: LoaderOutput<LoaderConfig>;
|
||||
tree?: PageTree.Root;
|
||||
}) {
|
||||
const tree =
|
||||
forcedTree ??
|
||||
(from._i18n
|
||||
let tree = forcedTree;
|
||||
|
||||
if (!tree) {
|
||||
tree = from._i18n
|
||||
? (from as LoaderOutput<LoaderConfig & { i18n: true }>).pageTree[
|
||||
page.locale ?? from._i18n.defaultLanguage
|
||||
]
|
||||
: from.pageTree);
|
||||
: from.pageTree;
|
||||
}
|
||||
|
||||
const parent = findParent(tree, page);
|
||||
if (!parent) return null;
|
||||
|
||||
@@ -98,3 +98,8 @@ export {
|
||||
useTreeContext,
|
||||
TreeContextProvider,
|
||||
} from './contexts/tree';
|
||||
export {
|
||||
StylesProvider,
|
||||
usePageStyles,
|
||||
type PageStyles,
|
||||
} from './contexts/layout';
|
||||
|
||||
Reference in New Issue
Block a user