mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core & UI: Move hide-if-empty component to Fumadocs Core
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Move `hide-if-empty` component to Fumadocs Core
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)))
|
||||
}}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user