UI: Expose --fd-tocnav-height CSS variable

This commit is contained in:
Fuma Nama
2024-12-14 21:28:28 +08:00
parent eb74b66a0c
commit 1a2597a550
17 changed files with 350 additions and 280 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'fumadocs-openapi': patch
'fumadocs-ui': patch
---
Expose `--fd-tocnav-height` CSS variable
+2 -2
View File
@@ -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
}
+9 -3
View File
@@ -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>
);
}
+1
View File
@@ -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 = {
+30
View File
@@ -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>
);
}
+2 -2
View File
@@ -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);
+2 -10
View File
@@ -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])}>
+44 -19
View File
@@ -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)}>
+28 -11
View File
@@ -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>
);
-47
View File
@@ -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>
);
}
+4
View File
@@ -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
View File
@@ -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>
);
}
+2 -23
View File
@@ -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
}
>
+24 -7
View File
@@ -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>
);
}
+36 -1
View File
@@ -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
View File
@@ -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;
+5
View File
@@ -98,3 +98,8 @@ export {
useTreeContext,
TreeContextProvider,
} from './contexts/tree';
export {
StylesProvider,
usePageStyles,
type PageStyles,
} from './contexts/layout';