Core & UI: Move hide-if-empty component to Fumadocs Core

This commit is contained in:
Fuma Nama
2025-06-02 20:32:44 +08:00
parent bedc2aec7b
commit b4916d2f5e
14 changed files with 332 additions and 419 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'fumadocs-core': patch
'fumadocs-ui': patch
---
Move `hide-if-empty` component to Fumadocs Core
+4
View File
@@ -28,6 +28,10 @@
"import": "./dist/content/index.js",
"types": "./dist/content/index.d.ts"
},
"./hide-if-empty": {
"import": "./dist/hide-if-empty.js",
"types": "./dist/hide-if-empty.d.ts"
},
"./search/*": {
"import": "./dist/search/*.js",
"types": "./dist/search/*.d.ts"
+12 -14
View File
@@ -1,6 +1,6 @@
'use client';
import React from 'react';
import type { ComponentProps, FC, ReactNode } from 'react';
import React from 'react';
import type { StaticImport } from 'next/dist/shared/lib/get-img-props';
export interface ImageProps extends Omit<ComponentProps<'img'>, 'src'> {
@@ -53,24 +53,22 @@ const FrameworkContext = createContext<Framework>('FrameworkContext', {
});
export function FrameworkProvider({
Link,
useRouter,
useParams,
usePathname,
Image,
children,
...props
}: Framework & { children: ReactNode }) {
const framework = React.useMemo(
() => ({
usePathname: props.usePathname,
useRouter: props.useRouter,
Link: props.Link,
Image: props.Image,
useParams: props.useParams,
usePathname,
useRouter,
Link,
Image,
useParams,
}),
[
props.Link,
props.usePathname,
props.useRouter,
props.useParams,
props.Image,
],
[Link, usePathname, useRouter, useParams, Image],
);
return (
+77
View File
@@ -0,0 +1,77 @@
'use client';
import React from 'react';
const isEmpty = (node: HTMLElement) => {
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes.item(i);
if (
child.nodeType === Node.TEXT_NODE ||
(child.nodeType === Node.ELEMENT_NODE &&
window.getComputedStyle(child as HTMLElement).display !== 'none')
) {
return false;
}
}
return true;
};
/**
* The built-in CSS `:empty` selector cannot detect if the children is hidden, classes such as `md:hidden` causes it to fail.
* This component is an enhancement to `empty:hidden` to fix the issue described above.
*
* This can be expensive, please avoid this whenever possible.
*/
export function HideIfEmpty({ children }: { children: React.ReactNode }) {
const id = React.useId();
const [empty, setEmpty] = React.useState<boolean | undefined>();
React.useEffect(() => {
const element = document.querySelector(
`[data-fdid="${id}"]`,
) as HTMLElement | null;
if (!element) return;
const onUpdate = () => {
setEmpty(isEmpty(element));
};
const observer = new ResizeObserver(onUpdate);
observer.observe(element);
onUpdate();
return () => {
observer.disconnect();
};
}, [id]);
let child;
if (React.isValidElement(children)) {
child = React.cloneElement(children, {
...(children.props as object),
'data-fdid': id,
'data-empty': empty,
} as object);
} else {
child =
React.Children.count(children) > 1 ? React.Children.only(null) : null;
}
return (
<>
{child}
{empty === undefined && (
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `{
const element = document.querySelector('[data-fdid="${id}"]')
if (element) {
element.setAttribute('data-empty', String((${isEmpty.toString()})(element)))
}}`,
}}
/>
)}
</>
);
}
-33
View File
@@ -1,33 +0,0 @@
export interface I18nConfig {
/**
* Supported locale codes.
*
* A page tree will be built for each language.
*/
languages: string[];
/**
* Default locale if not specified
*/
defaultLanguage: string;
/**
* Don't show the locale prefix on URL.
*
* - `always`: Always hide the prefix
* - `default-locale`: Only hide the default locale
* - `never`: Never hide the prefix
*
* This API uses `NextResponse.rewrite`.
*
* @defaultValue 'never'
*/
hideLocale?: 'always' | 'default-locale' | 'never';
/**
* Used by `loader()`, specify the way to parse i18n file structure.
*
* @defaultValue 'dot'
*/
parser?: 'dot' | 'dir';
}
+34 -1
View File
@@ -1,2 +1,35 @@
export * from './config';
export * from './middleware';
export interface I18nConfig {
/**
* Supported locale codes.
*
* A page tree will be built for each language.
*/
languages: string[];
/**
* Default locale if not specified
*/
defaultLanguage: string;
/**
* Don't show the locale prefix on URL.
*
* - `always`: Always hide the prefix
* - `default-locale`: Only hide the default locale
* - `never`: Never hide the prefix
*
* This API uses `NextResponse.rewrite`.
*
* @defaultValue 'never'
*/
hideLocale?: 'always' | 'default-locale' | 'never';
/**
* Used by `loader()`, specify the way to parse i18n file structure.
*
* @defaultValue 'dot'
*/
parser?: 'dot' | 'dir';
}
+1 -1
View File
@@ -14,7 +14,7 @@ export default defineConfig({
target: 'es2022',
format: 'esm',
entry: [
'src/{toc,link,breadcrumb,sidebar,dynamic-link}.tsx',
'src/{toc,link,breadcrumb,sidebar,dynamic-link,hide-if-empty}.tsx',
'src/{server,source,mdx-plugins,content}/index.ts',
'src/search/client.ts',
'src/search/server.ts',
@@ -1,91 +0,0 @@
'use client';
import { Slot } from '@radix-ui/react-slot';
import { useEffect, useId, useRef, useState } from 'react';
function isEmpty(node: HTMLElement) {
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes.item(i);
if (child.nodeType === Node.TEXT_NODE) {
return false;
} else if (
child.nodeType === Node.ELEMENT_NODE &&
window.getComputedStyle(child as HTMLElement).display !== 'none'
) {
return false;
}
}
return true;
}
/**
* The built-in CSS `:empty` selector cannot detect if the children is hidden, classes such as `md:hidden` causes it to fail.
* This component is an enhancement to `empty:hidden` to fix the issue described above.
*
* This can be expensive, please avoid this whenever possible.
*/
export function HideIfEmpty({ children }: { children: React.ReactNode }) {
const id = useId();
const ref = useRef<HTMLDivElement>(null);
const [empty, setEmpty] = useState<boolean | undefined>();
useEffect(() => {
const element = ref.current;
if (!element) return;
const onUpdate = () => {
setEmpty(isEmpty(element));
};
const observer = new ResizeObserver(onUpdate);
observer.observe(element);
onUpdate();
return () => {
observer.disconnect();
};
}, []);
const inject = `{
function isEmpty(node) {
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes.item(i);
if (child.nodeType === Node.TEXT_NODE) {
return false;
} else if (
child.nodeType === Node.ELEMENT_NODE &&
window.getComputedStyle(child).display !== 'none'
) {
return false;
}
}
return true;
}
const element = document.querySelector('[data-fdid="${id}"]')
if (element) {
element.setAttribute('data-empty', String(isEmpty(element)))
}}`;
return (
<>
<Slot
ref={ref}
data-fdid={id}
data-empty={empty}
className="data-[empty=true]:hidden"
suppressHydrationWarning
>
{children}
</Slot>
{empty === undefined && (
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: inject,
}}
/>
)}
</>
);
}
+97 -143
View File
@@ -11,12 +11,7 @@ import {
SidebarPageTree,
SidebarViewport,
} from '@/components/layout/sidebar';
import { omit, slot, slots } from '@/layouts/shared';
import {
BaseLinkItem,
type IconItemType,
type LinkItemType,
} from '@/layouts/links';
import { BaseLinkItem, type LinkItemType } from '@/layouts/links';
import { RootToggle } from '@/components/layout/root-toggle';
import { type BaseLayoutProps, getLinks } from './shared';
import {
@@ -42,7 +37,7 @@ import {
LargeSearchToggle,
SearchToggle,
} from '@/components/layout/search-toggle';
import { HideIfEmpty } from '@/components/ui/hide-if-empty';
import { HideIfEmpty } from 'fumadocs-core/hide-if-empty';
export interface DocsLayoutProps extends BaseLayoutProps {
tree: PageTree.Root;
@@ -60,17 +55,24 @@ export interface DocsLayoutProps extends BaseLayoutProps {
export function DocsLayout({
nav: { transparentMode, ...nav } = {},
sidebar = {},
searchToggle,
sidebar: {
tabs: sidebarTabs,
enabled: sidebarEnabled = true,
collapsible: sidebarCollapsible = true,
component: sidebarComponent,
components: sidebarComponents,
...sidebar
} = {},
searchToggle = {},
disableThemeSwitch = false,
themeSwitch = { enabled: !disableThemeSwitch },
i18n = false,
children,
...props
}: DocsLayoutProps): ReactNode {
}: DocsLayoutProps) {
const tabs = useMemo(
() => getSidebarTabsFromOptions(sidebar.tabs, props.tree) ?? [],
[sidebar.tabs, props.tree],
() => getSidebarTabsFromOptions(sidebarTabs, props.tree) ?? [],
[sidebarTabs, props.tree],
);
const links = getLinks(props.links ?? [], props.githubUrl);
@@ -81,115 +83,75 @@ export function DocsLayout({
: undefined,
);
return (
<TreeContextProvider tree={props.tree}>
<NavProvider transparentMode={transparentMode}>
{slot(
nav,
<Navbar className="h-14 md:hidden">
<Link
href={nav.url ?? '/'}
className="inline-flex items-center gap-2.5 font-semibold"
>
{nav.title}
</Link>
<div className="flex-1">{nav.children}</div>
{slots(
'sm',
searchToggle,
<SearchToggle className="p-2" hideIfDisabled />,
)}
<NavbarSidebarTrigger className="p-2 -me-1.5 md:hidden" />
</Navbar>,
)}
<LayoutBody
{...props.containerProps}
className={cn(variables, props.containerProps?.className)}
>
{slot(
sidebar,
<DocsLayoutSidebar
{...omit(sidebar, 'enabled', 'component', 'tabs')}
links={links}
nav={
<>
<Link
href={nav.url ?? '/'}
className="inline-flex text-[15px] items-center gap-2.5 font-medium"
>
{nav.title}
</Link>
{nav.children}
</>
}
banner={
<>
{tabs.length > 0 ? <RootToggle options={tabs} /> : null}
{slots(
'lg',
searchToggle,
<LargeSearchToggle
hideIfDisabled
className="max-md:hidden"
/>,
)}
{sidebar.banner}
</>
}
footer={
<>
<DocsLayoutSidebarFooter
links={links.filter((item) => item.type === 'icon')}
i18n={i18n}
themeSwitch={themeSwitch}
/>
{sidebar.footer}
</>
}
/>,
)}
{children}
</LayoutBody>
</NavProvider>
</TreeContextProvider>
const sidebarFooter = (
<HideIfEmpty>
<div className="flex items-center justify-end">
<div className="flex items-center flex-1 empty: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 md:[&_svg]:size-4.5',
)}
aria-label={item.label}
>
{item.icon}
</BaseLinkItem>
))}
</div>
{i18n ? (
<LanguageToggle className="me-1.5">
<Languages className="size-4.5" />
<LanguageToggleText className="md:hidden" />
</LanguageToggle>
) : null}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle className="p-0" mode={themeSwitch.mode} />
))}
</div>
</HideIfEmpty>
);
}
export function DocsLayoutSidebar({
collapsible = true,
components,
nav,
links = [],
footer,
banner,
...props
}: Omit<SidebarOptions, 'tabs'> & {
links?: LinkItemType[];
nav?: ReactNode;
}) {
return (
sidebarComponent ??= (
<>
{collapsible ? <CollapsibleControl /> : null}
<Sidebar {...props} collapsible={collapsible}>
{sidebarCollapsible ? <CollapsibleControl /> : null}
<Sidebar {...sidebar} collapsible={sidebarCollapsible}>
<HideIfEmpty>
<SidebarHeader>
<div className="flex max-md:hidden">
{nav}
{collapsible && (
<Link
href={nav.url ?? '/'}
className="inline-flex text-[15px] items-center gap-2.5 font-medium"
>
{nav.title}
</Link>
{nav.children}
{sidebarCollapsible && (
<SidebarCollapseTrigger
className={cn(
buttonVariants({
color: 'ghost',
size: 'icon-sm',
className:
'ms-auto mb-auto text-fd-muted-foreground max-md:hidden',
}),
'ms-auto mb-auto text-fd-muted-foreground max-md:hidden',
)}
>
<SidebarIcon />
</SidebarCollapseTrigger>
)}
</div>
{banner}
{tabs.length > 0 && <RootToggle options={tabs} />}
{searchToggle.enabled !== false &&
(searchToggle.components?.lg ?? (
<LargeSearchToggle hideIfDisabled className="max-md:hidden" />
))}
{sidebar.banner}
</SidebarHeader>
</HideIfEmpty>
<SidebarViewport>
@@ -202,55 +164,47 @@ export function DocsLayoutSidebar({
className={cn(i === list.length - 1 && 'mb-4')}
/>
))}
<SidebarPageTree components={components} />
<SidebarPageTree components={sidebarComponents} />
</SidebarViewport>
<HideIfEmpty>
<SidebarFooter>{footer}</SidebarFooter>
<SidebarFooter>
{sidebarFooter}
{sidebar.footer}
</SidebarFooter>
</HideIfEmpty>
</Sidebar>
</>
);
}
export function DocsLayoutSidebarFooter({
i18n,
themeSwitch,
links = [],
}: {
i18n?: DocsLayoutProps['i18n'];
links?: IconItemType[];
themeSwitch?: DocsLayoutProps['themeSwitch'];
}) {
return (
<HideIfEmpty>
<div className="flex items-center justify-end">
<div className="flex items-center flex-1 empty:hidden">
{links.map((item, i) => (
<BaseLinkItem
key={i}
item={item}
className={cn(
buttonVariants({ size: 'icon', color: 'ghost' }),
'text-fd-muted-foreground md:[&_svg]:size-4.5',
)}
aria-label={item.label}
>
{item.icon}
</BaseLinkItem>
<TreeContextProvider tree={props.tree}>
<NavProvider transparentMode={transparentMode}>
{nav.enabled !== false &&
(nav.component ?? (
<Navbar className="h-14 md:hidden">
<Link
href={nav.url ?? '/'}
className="inline-flex items-center gap-2.5 font-semibold"
>
{nav.title}
</Link>
<div className="flex-1">{nav.children}</div>
{searchToggle?.enabled !== false &&
(searchToggle.components?.sm ?? (
<SearchToggle className="p-2" hideIfDisabled />
))}
<NavbarSidebarTrigger className="p-2 -me-1.5 md:hidden" />
</Navbar>
))}
</div>
{i18n ? (
<LanguageToggle className="me-1.5">
<Languages className="size-4.5" />
<LanguageToggleText className="md:hidden" />
</LanguageToggle>
) : null}
{slot(
themeSwitch,
<ThemeToggle className="p-0" mode={themeSwitch?.mode} />,
)}
</div>
</HideIfEmpty>
<LayoutBody
{...props.containerProps}
className={cn(variables, props.containerProps?.className)}
>
{sidebarEnabled && sidebarComponent}
{children}
</LayoutBody>
</NavProvider>
</TreeContextProvider>
);
}
+1 -1
View File
@@ -14,7 +14,7 @@ import {
type RootProps,
} from './page-client';
import { TOCItems, TOCScrollArea } from '@/components/layout/toc';
import { Text } from '@/icons';
import { Text } from 'lucide-react';
import { I18nLabel } from '@/contexts/i18n';
import ClerkTOCItems from '@/components/layout/toc-clerk';
+35 -33
View File
@@ -1,7 +1,6 @@
import { Fragment, type HTMLAttributes, useMemo } from 'react';
import { type NavOptions, slot, slots } from '@/layouts/shared';
import { cn } from '@/utils/cn';
import { type BaseLayoutProps, getLinks } from './shared';
import { type BaseLayoutProps, getLinks, type NavOptions } from './shared';
import { NavProvider } from '@/contexts/layout';
import {
Navbar,
@@ -46,7 +45,7 @@ export function HomeLayout(
props: HomeLayoutProps & HTMLAttributes<HTMLElement>,
) {
const {
nav,
nav = {},
links,
githubUrl,
i18n,
@@ -63,17 +62,17 @@ export function HomeLayout(
{...rest}
className={cn('flex flex-1 flex-col pt-14', rest.className)}
>
{slot(
nav,
<Header
links={links}
nav={nav}
themeSwitch={themeSwitch}
searchToggle={searchToggle}
i18n={i18n}
githubUrl={githubUrl}
/>,
)}
{nav.enabled !== false &&
(nav.component ?? (
<Header
links={links}
nav={nav}
themeSwitch={themeSwitch}
searchToggle={searchToggle}
i18n={i18n}
githubUrl={githubUrl}
/>
))}
{props.children}
</main>
</NavProvider>
@@ -85,8 +84,8 @@ export function Header({
i18n = false,
links,
githubUrl,
themeSwitch,
searchToggle,
themeSwitch = {},
searchToggle = {},
}: HomeLayoutProps) {
const finalLinks = useMemo(
() => getLinks(links, githubUrl),
@@ -117,23 +116,23 @@ export function Header({
))}
</ul>
<div className="flex flex-row items-center justify-end gap-1.5 flex-1">
{slots(
'sm',
searchToggle,
<SearchToggle className="p-2 lg:hidden" hideIfDisabled />,
)}
{slots(
'lg',
searchToggle,
<LargeSearchToggle
className="w-full rounded-full ps-2.5 max-w-[240px] max-lg:hidden"
hideIfDisabled
/>,
)}
{slot(
themeSwitch,
<ThemeToggle className="max-lg:hidden" mode={themeSwitch?.mode} />,
{searchToggle.enabled !== false && (
<>
{searchToggle.components?.sm ?? (
<SearchToggle className="p-2 lg:hidden" hideIfDisabled />
)}
{searchToggle.components?.lg ?? (
<LargeSearchToggle
className="w-full rounded-full ps-2.5 max-w-[240px] max-lg:hidden"
hideIfDisabled
/>
)}
</>
)}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle className="max-lg:hidden" mode={themeSwitch?.mode} />
))}
{i18n ? (
<LanguageToggle className="max-lg:hidden">
<Languages className="size-5" />
@@ -180,7 +179,10 @@ export function Header({
<ChevronDown className="size-3 text-fd-muted-foreground" />
</LanguageToggle>
) : null}
{slot(themeSwitch, <ThemeToggle mode={themeSwitch?.mode} />)}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle mode={themeSwitch?.mode} />
))}
</div>
</MenuContent>
</Menu>
+34 -34
View File
@@ -1,5 +1,5 @@
import { Fragment, type HTMLAttributes, useMemo } from 'react';
import { type BaseLayoutProps, getLinks, slot, slots } from '@/layouts/shared';
import { type BaseLayoutProps, getLinks } from '@/layouts/shared';
import {
Sidebar,
SidebarCollapseTrigger,
@@ -40,7 +40,7 @@ import {
LargeSearchToggle,
SearchToggle,
} from '@/components/layout/search-toggle';
import { HideIfEmpty } from '@/components/ui/hide-if-empty';
import { HideIfEmpty } from 'fumadocs-core/hide-if-empty';
export interface DocsLayoutProps extends BaseLayoutProps {
tree: PageTree.Root;
@@ -179,13 +179,13 @@ export function DocsLayout(props: DocsLayoutProps) {
<Languages className="size-4.5 text-fd-muted-foreground" />
</LanguageToggle>
) : null}
{slot(
themeSwitch,
<ThemeToggle
className="md:hidden"
mode={themeSwitch?.mode ?? 'light-dark-system'}
/>,
)}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle
className="md:hidden"
mode={themeSwitch?.mode ?? 'light-dark-system'}
/>
))}
{sidebarFooter}
</SidebarFooter>
</HideIfEmpty>
@@ -205,6 +205,8 @@ export function DocsLayout(props: DocsLayoutProps) {
function DocsNavbar({
links,
tabs,
searchToggle = {},
themeSwitch = {},
...props
}: DocsLayoutProps & {
links: LinkItemType[];
@@ -253,19 +255,18 @@ function DocsNavbar({
) : null}
{nav}
</div>
{slots(
'lg',
props.searchToggle,
<LargeSearchToggle
hideIfDisabled
className={cn(
'w-full my-auto max-md:hidden',
navMode === 'top'
? 'rounded-xl max-w-sm ps-2.5'
: 'max-w-[240px]',
)}
/>,
)}
{searchToggle.enabled !== false &&
(searchToggle.components?.lg ?? (
<LargeSearchToggle
hideIfDisabled
className={cn(
'w-full my-auto max-md:hidden',
navMode === 'top'
? 'rounded-xl max-w-sm ps-2.5'
: 'max-w-[240px]',
)}
/>
))}
<div className="flex flex-1 flex-row items-center justify-end">
<div className="flex flex-row items-center gap-6 px-4 empty:hidden max-lg:hidden">
{links
@@ -279,11 +280,10 @@ function DocsNavbar({
))}
</div>
{props.nav?.children}
{slots(
'sm',
props.searchToggle,
<SearchToggle hideIfDisabled className="p-2 md:hidden" />,
)}
{searchToggle.enabled !== false &&
(searchToggle.components?.sm ?? (
<SearchToggle hideIfDisabled className="p-2 md:hidden" />
))}
<NavbarSidebarTrigger className="p-2 -me-1.5 md:hidden" />
{links
.filter((item) => item.type === 'icon')
@@ -305,13 +305,13 @@ function DocsNavbar({
<Languages className="size-4.5 text-fd-muted-foreground" />
</LanguageToggle>
) : null}
{slot(
props.themeSwitch,
<ThemeToggle
className="ms-2 max-md:hidden"
mode={props.themeSwitch?.mode ?? 'light-dark-system'}
/>,
)}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle
className="ms-2 max-md:hidden"
mode={themeSwitch.mode ?? 'light-dark-system'}
/>
))}
{sidebarCollapsible && navMode === 'top' ? (
<SidebarCollapseTrigger
className={cn(
-42
View File
@@ -91,45 +91,3 @@ export function getLinks(
return result;
}
export function slot(
obj:
| {
enabled?: boolean;
component?: ReactNode;
}
| undefined,
def: ReactNode,
): ReactNode {
if (obj?.enabled === false) return;
if (obj?.component !== undefined) return obj.component;
return def;
}
export function slots<Comp extends Record<string, ReactNode>>(
variant: keyof Comp,
obj:
| {
enabled?: boolean;
components?: Comp;
}
| undefined,
def: ReactNode,
): ReactNode {
if (obj?.enabled === false) return;
if (obj?.components?.[variant] !== undefined) return obj.components[variant];
return def;
}
export function omit<T extends Record<string, unknown>, Keys extends keyof T>(
obj: T,
...keys: Keys[]
): Omit<T, Keys> {
const clone = { ...obj };
for (const key of keys) {
delete clone[key];
}
return clone;
}
+31 -26
View File
@@ -3,7 +3,6 @@ import { cn } from '@/utils/cn';
import { buttonVariants } from '@/components/ui/button';
import { Edit } from '@/icons';
import { I18nLabel } from '@/contexts/i18n';
import { slot } from '@/layouts/shared';
import {
type BreadcrumbProps,
type FooterProps,
@@ -113,14 +112,18 @@ type TableOfContentPopoverOptions = Omit<TableOfContentOptions, 'single'>;
export function DocsPage({
editOnGithub,
breadcrumb,
footer,
breadcrumb: {
enabled: breadcrumbEnabled = true,
component: breadcrumb,
...breadcrumbProps
} = {},
footer = {},
lastUpdate,
container,
full = false,
tableOfContentPopover: {
enabled: tocPopoverEnabled,
component: tocPopoverReplace,
component: tocPopover,
...tocPopoverOptions
} = {},
tableOfContent: {
@@ -153,19 +156,20 @@ export function DocsPage({
}}
{...container}
>
{slot(
{ enabled: tocPopoverEnabled, component: tocPopoverReplace },
<PageTOCPopover>
<PageTOCPopoverTrigger />
<PageTOCPopoverContent>
{tocPopoverOptions.header}
<PageTOCPopoverItems variant={tocPopoverOptions.style} />
{tocPopoverOptions.footer}
</PageTOCPopoverContent>
</PageTOCPopover>,
)}
{tocPopoverEnabled &&
(tocPopover ?? (
<PageTOCPopover>
<PageTOCPopoverTrigger />
<PageTOCPopoverContent>
{tocPopoverOptions.header}
<PageTOCPopoverItems variant={tocPopoverOptions.style} />
{tocPopoverOptions.footer}
</PageTOCPopoverContent>
</PageTOCPopover>
))}
<PageArticle {...article}>
{slot(breadcrumb, <PageBreadcrumb {...breadcrumb} />)}
{breadcrumbEnabled &&
(breadcrumb ?? <PageBreadcrumb {...breadcrumbProps} />)}
{children}
<div role="none" className="flex-1" />
<div className="flex flex-row flex-wrap items-center justify-between gap-4 empty:hidden">
@@ -176,17 +180,18 @@ export function DocsPage({
)}
{lastUpdate && <PageLastUpdate date={new Date(lastUpdate)} />}
</div>
{slot(footer, <PageFooter items={footer?.items} />)}
{footer.enabled !== false &&
(footer.component ?? <PageFooter items={footer.items} />)}
</PageArticle>
{slot(
{ enabled: tocEnabled, component: tocReplace },
<PageTOC>
{tocOptions.header}
<PageTOCTitle />
<PageTOCItems variant={tocOptions.style} />
{tocOptions.footer}
</PageTOC>,
)}
{tocEnabled &&
(tocReplace ?? (
<PageTOC>
{tocOptions.header}
<PageTOCTitle />
<PageTOCItems variant={tocOptions.style} />
{tocOptions.footer}
</PageTOC>
))}
</PageRoot>
);
}