mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
UI: Support searchToggle option to customise search toggle
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Support `searchToggle` option to customise search toggle
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
SidebarViewport,
|
||||
SidebarPageTree,
|
||||
} from '@/components/layout/sidebar';
|
||||
import { replaceOrDefault } from '@/layouts/shared';
|
||||
import { slot, slots } from '@/layouts/shared';
|
||||
import {
|
||||
type LinkItemType,
|
||||
BaseLinkItem,
|
||||
@@ -63,23 +63,24 @@ export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
}
|
||||
|
||||
export function DocsLayout({
|
||||
nav: {
|
||||
enabled: navEnabled = true,
|
||||
component: navReplace,
|
||||
transparentMode,
|
||||
...nav
|
||||
} = {},
|
||||
themeSwitch,
|
||||
sidebar = {},
|
||||
nav: { transparentMode, ...nav } = {},
|
||||
searchToggle,
|
||||
disableThemeSwitch = false,
|
||||
themeSwitch = { enabled: !disableThemeSwitch },
|
||||
sidebar: { tabs: tabOptions, ...sidebar } = {},
|
||||
i18n = false,
|
||||
children,
|
||||
...props
|
||||
}: DocsLayoutProps): ReactNode {
|
||||
const tabs = useMemo(
|
||||
() => getSidebarTabsFromOptions(tabOptions, props.tree) ?? [],
|
||||
[tabOptions, props.tree],
|
||||
);
|
||||
const links = getLinks(props.links ?? [], props.githubUrl);
|
||||
|
||||
const variables = cn(
|
||||
'[--fd-tocnav-height:36px] md:[--fd-sidebar-width:268px] lg:[--fd-sidebar-width:286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px]',
|
||||
!navReplace && navEnabled
|
||||
!nav.component && nav.enabled !== false
|
||||
? '[--fd-nav-height:calc(var(--spacing)*14)] md:[--fd-nav-height:0px]'
|
||||
: undefined,
|
||||
);
|
||||
@@ -92,8 +93,8 @@ export function DocsLayout({
|
||||
return (
|
||||
<TreeContextProvider tree={props.tree}>
|
||||
<NavProvider transparentMode={transparentMode}>
|
||||
{replaceOrDefault(
|
||||
{ enabled: navEnabled, component: navReplace },
|
||||
{slot(
|
||||
nav,
|
||||
<Navbar className="md:hidden">
|
||||
<Link
|
||||
href={nav.url ?? '/'}
|
||||
@@ -104,10 +105,9 @@ export function DocsLayout({
|
||||
<div className="flex flex-1 flex-row items-center gap-1">
|
||||
{nav.children}
|
||||
</div>
|
||||
<SearchToggle hideIfDisabled />
|
||||
{slots('sm', searchToggle, <SearchToggle hideIfDisabled />)}
|
||||
<NavbarSidebarTrigger className="-me-2 md:hidden" />
|
||||
</Navbar>,
|
||||
nav,
|
||||
)}
|
||||
<main
|
||||
id="nd-docs-layout"
|
||||
@@ -122,10 +122,9 @@ export function DocsLayout({
|
||||
...props.containerProps?.style,
|
||||
}}
|
||||
>
|
||||
{replaceOrDefault(
|
||||
{slot(
|
||||
sidebar,
|
||||
<DocsLayoutSidebar
|
||||
tree={props.tree}
|
||||
{...sidebar}
|
||||
links={links}
|
||||
nav={
|
||||
@@ -139,6 +138,22 @@ export function DocsLayout({
|
||||
{nav.children}
|
||||
</>
|
||||
}
|
||||
banner={
|
||||
<>
|
||||
{tabs.length > 0 ? (
|
||||
<RootToggle options={tabs} className="-mx-2" />
|
||||
) : null}
|
||||
{slots(
|
||||
'lg',
|
||||
searchToggle,
|
||||
<LargeSearchToggle
|
||||
hideIfDisabled
|
||||
className="rounded-lg max-md:hidden"
|
||||
/>,
|
||||
)}
|
||||
{sidebar.banner}
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<DocsLayoutSidebarFooter
|
||||
@@ -161,23 +176,15 @@ export function DocsLayout({
|
||||
export function DocsLayoutSidebar({
|
||||
collapsible = true,
|
||||
components,
|
||||
tabs: tabOptions,
|
||||
hideSearch,
|
||||
tree,
|
||||
nav,
|
||||
links = [],
|
||||
footer,
|
||||
banner,
|
||||
...props
|
||||
}: Partial<SidebarOptions> & {
|
||||
tree: PageTree.Root;
|
||||
}: Omit<SidebarOptions, 'tabs'> & {
|
||||
links?: LinkItemType[];
|
||||
nav?: ReactNode;
|
||||
}) {
|
||||
const tabs = useMemo(
|
||||
() => getSidebarTabsFromOptions(tabOptions, tree) ?? [],
|
||||
[tabOptions, tree],
|
||||
);
|
||||
const Aside = collapsible ? CollapsibleSidebar : Sidebar;
|
||||
|
||||
return (
|
||||
@@ -205,15 +212,6 @@ export function DocsLayoutSidebar({
|
||||
)}
|
||||
</div>
|
||||
{banner}
|
||||
{tabs.length > 0 ? (
|
||||
<RootToggle options={tabs} className="-mx-2" />
|
||||
) : null}
|
||||
{!hideSearch ? (
|
||||
<LargeSearchToggle
|
||||
hideIfDisabled
|
||||
className="rounded-lg max-md:hidden"
|
||||
/>
|
||||
) : null}
|
||||
</SidebarHeader>
|
||||
<SidebarViewport>
|
||||
<div className="mb-4 empty:hidden">
|
||||
@@ -266,7 +264,7 @@ export function DocsLayoutSidebarFooter({
|
||||
<LanguageToggleText className="md:hidden" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
{replaceOrDefault(
|
||||
{slot(
|
||||
themeSwitch,
|
||||
<ThemeToggle className="p-0" mode={themeSwitch?.mode} />,
|
||||
)}
|
||||
|
||||
@@ -31,13 +31,6 @@ export interface SidebarOptions extends SidebarProps {
|
||||
|
||||
banner?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* Hide search trigger. You can also disable search for the entire site from `<RootProvider />`.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
hideSearch?: boolean;
|
||||
}
|
||||
|
||||
export function SidebarLinkItem({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment, type HTMLAttributes } from 'react';
|
||||
import { type NavOptions, replaceOrDefault } from '@/layouts/shared';
|
||||
import { Fragment, type HTMLAttributes, useMemo } from 'react';
|
||||
import { type NavOptions, slot, slots } from '@/layouts/shared';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getLinks, type BaseLayoutProps } from './shared';
|
||||
import { NavProvider } from '@/contexts/layout';
|
||||
@@ -30,9 +30,7 @@ import {
|
||||
MenuTrigger,
|
||||
} from '@/layouts/home/menu';
|
||||
|
||||
export interface HomeLayoutProps
|
||||
extends BaseLayoutProps,
|
||||
HTMLAttributes<HTMLElement> {
|
||||
export interface HomeLayoutProps extends BaseLayoutProps {
|
||||
nav?: Partial<
|
||||
NavOptions & {
|
||||
/**
|
||||
@@ -43,19 +41,20 @@ export interface HomeLayoutProps
|
||||
>;
|
||||
}
|
||||
|
||||
export function HomeLayout(props: HomeLayoutProps) {
|
||||
export function HomeLayout(
|
||||
props: HomeLayoutProps & HTMLAttributes<HTMLElement>,
|
||||
) {
|
||||
const {
|
||||
nav,
|
||||
links,
|
||||
githubUrl,
|
||||
i18n: _i18n,
|
||||
themeSwitch: _themeSwitch,
|
||||
disableThemeSwitch: _disableThemeSwitch,
|
||||
i18n,
|
||||
disableThemeSwitch = false,
|
||||
themeSwitch = { enabled: !disableThemeSwitch },
|
||||
searchToggle,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const finalLinks = getLinks(links, githubUrl);
|
||||
|
||||
return (
|
||||
<NavProvider transparentMode={nav?.transparentMode}>
|
||||
<main
|
||||
@@ -63,23 +62,36 @@ export function HomeLayout(props: HomeLayoutProps) {
|
||||
{...rest}
|
||||
className={cn('flex flex-1 flex-col pt-14', rest.className)}
|
||||
>
|
||||
{replaceOrDefault(nav, <Header finalLinks={finalLinks} {...props} />, {
|
||||
items: finalLinks,
|
||||
})}
|
||||
{slot(
|
||||
nav,
|
||||
<Header
|
||||
links={links}
|
||||
nav={nav}
|
||||
themeSwitch={themeSwitch}
|
||||
searchToggle={searchToggle}
|
||||
i18n={i18n}
|
||||
githubUrl={githubUrl}
|
||||
/>,
|
||||
)}
|
||||
{props.children}
|
||||
</main>
|
||||
</NavProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Header({
|
||||
nav: { enableSearch = true, ...nav } = {},
|
||||
export function Header({
|
||||
nav = {},
|
||||
i18n = false,
|
||||
finalLinks,
|
||||
links,
|
||||
githubUrl,
|
||||
themeSwitch,
|
||||
}: HomeLayoutProps & {
|
||||
finalLinks: LinkItemType[];
|
||||
}) {
|
||||
searchToggle,
|
||||
}: HomeLayoutProps) {
|
||||
const finalLinks = useMemo(
|
||||
() => getLinks(links, githubUrl),
|
||||
[links, githubUrl],
|
||||
);
|
||||
|
||||
const navItems = finalLinks.filter((item) =>
|
||||
['nav', 'all'].includes(item.on ?? 'all'),
|
||||
);
|
||||
@@ -104,16 +116,20 @@ function Header({
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-row items-center justify-end gap-1.5 flex-1">
|
||||
{enableSearch ? (
|
||||
<>
|
||||
<SearchToggle className="lg:hidden" hideIfDisabled />
|
||||
<LargeSearchToggle
|
||||
className="w-full max-w-[240px] max-lg:hidden"
|
||||
hideIfDisabled
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{replaceOrDefault(
|
||||
{slots(
|
||||
'sm',
|
||||
searchToggle,
|
||||
<SearchToggle className="lg:hidden" hideIfDisabled />,
|
||||
)}
|
||||
{slots(
|
||||
'lg',
|
||||
searchToggle,
|
||||
<LargeSearchToggle
|
||||
className="w-full max-w-[240px] max-lg:hidden"
|
||||
hideIfDisabled
|
||||
/>,
|
||||
)}
|
||||
{slot(
|
||||
themeSwitch,
|
||||
<ThemeToggle className="max-lg:hidden" mode={themeSwitch?.mode} />,
|
||||
)}
|
||||
@@ -157,10 +173,7 @@ function Header({
|
||||
<ChevronDown className="size-3 text-fd-muted-foreground" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
{replaceOrDefault(
|
||||
themeSwitch,
|
||||
<ThemeToggle mode={themeSwitch?.mode} />,
|
||||
)}
|
||||
{slot(themeSwitch, <ThemeToggle mode={themeSwitch?.mode} />)}
|
||||
</div>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Fragment, type HTMLAttributes, useMemo } from 'react';
|
||||
import {
|
||||
type BaseLayoutProps,
|
||||
getLinks,
|
||||
replaceOrDefault,
|
||||
} from '@/layouts/shared';
|
||||
import { Fragment, type HTMLAttributes, type ReactNode, useMemo } from 'react';
|
||||
import { type BaseLayoutProps, getLinks, slot, slots } from '@/layouts/shared';
|
||||
import {
|
||||
CollapsibleSidebar,
|
||||
Sidebar,
|
||||
@@ -14,10 +10,6 @@ import {
|
||||
SidebarPageTree,
|
||||
} from '@/components/layout/sidebar';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import {
|
||||
LargeSearchToggle,
|
||||
SearchToggle,
|
||||
} from '@/components/layout/search-toggle';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { ChevronDown, Languages, SidebarIcon } from 'lucide-react';
|
||||
@@ -50,6 +42,10 @@ import {
|
||||
} from '@/contexts/layout';
|
||||
import { type Option, RootToggle } from '@/components/layout/root-toggle';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import {
|
||||
LargeSearchToggle,
|
||||
SearchToggle,
|
||||
} from '@/components/layout/search-toggle';
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
@@ -76,7 +72,9 @@ export function DocsLayout({
|
||||
...sidebar
|
||||
} = {},
|
||||
i18n = false,
|
||||
themeSwitch,
|
||||
disableThemeSwitch = false,
|
||||
themeSwitch = { enabled: !disableThemeSwitch },
|
||||
searchToggle,
|
||||
...props
|
||||
}: DocsLayoutProps) {
|
||||
const navMode = nav.mode ?? 'auto';
|
||||
@@ -189,7 +187,7 @@ export function DocsLayout({
|
||||
<Languages className="size-5 text-fd-muted-foreground" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
{replaceOrDefault(
|
||||
{slot(
|
||||
themeSwitch,
|
||||
<ThemeToggle
|
||||
className="md:hidden"
|
||||
@@ -200,12 +198,26 @@ export function DocsLayout({
|
||||
</SidebarFooter>
|
||||
</Aside>
|
||||
<DocsNavbar
|
||||
nav={nav}
|
||||
mode={nav.mode}
|
||||
nav={
|
||||
<Link
|
||||
href={nav.url ?? '/'}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2.5 font-semibold',
|
||||
navMode === 'auto' && 'md:hidden',
|
||||
)}
|
||||
>
|
||||
{nav.title}
|
||||
</Link>
|
||||
}
|
||||
links={links}
|
||||
i18n={i18n}
|
||||
sidebarCollapsible={sidebarCollapsible}
|
||||
searchToggle={searchToggle}
|
||||
tabs={tabMode == 'navbar' ? tabs : []}
|
||||
/>
|
||||
>
|
||||
{nav.children}
|
||||
</DocsNavbar>
|
||||
<StylesProvider {...pageStyles}>{props.children}</StylesProvider>
|
||||
</main>
|
||||
</NavProvider>
|
||||
@@ -214,22 +226,28 @@ export function DocsLayout({
|
||||
}
|
||||
|
||||
function DocsNavbar({
|
||||
sidebarCollapsible,
|
||||
mode: navMode = 'auto',
|
||||
nav,
|
||||
sidebarCollapsible = false,
|
||||
links,
|
||||
themeSwitch,
|
||||
nav = {},
|
||||
i18n,
|
||||
searchToggle,
|
||||
i18n = false,
|
||||
tabs,
|
||||
children,
|
||||
}: {
|
||||
nav: DocsLayoutProps['nav'];
|
||||
sidebarCollapsible: boolean;
|
||||
i18n: Required<DocsLayoutProps>['i18n'];
|
||||
mode?: 'top' | 'auto';
|
||||
nav?: ReactNode;
|
||||
sidebarCollapsible?: boolean;
|
||||
|
||||
i18n?: DocsLayoutProps['i18n'];
|
||||
themeSwitch?: DocsLayoutProps['themeSwitch'];
|
||||
searchToggle?: DocsLayoutProps['searchToggle'];
|
||||
|
||||
links: LinkItemType[];
|
||||
tabs: Option[];
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const navMode = nav.mode ?? 'auto';
|
||||
|
||||
return (
|
||||
<Navbar mode={navMode}>
|
||||
<div
|
||||
@@ -257,25 +275,19 @@ function DocsNavbar({
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
) : null}
|
||||
<Link
|
||||
href={nav.url ?? '/'}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2.5 font-semibold',
|
||||
navMode === 'auto' && 'md:hidden',
|
||||
)}
|
||||
>
|
||||
{nav.title}
|
||||
</Link>
|
||||
{nav}
|
||||
</div>
|
||||
|
||||
<LargeSearchToggle
|
||||
hideIfDisabled
|
||||
className={cn(
|
||||
'w-full my-auto rounded-xl max-md:hidden',
|
||||
navMode === 'top' ? 'max-w-sm px-2' : 'max-w-[240px]',
|
||||
)}
|
||||
/>
|
||||
|
||||
{slots(
|
||||
'lg',
|
||||
searchToggle,
|
||||
<LargeSearchToggle
|
||||
hideIfDisabled
|
||||
className={cn(
|
||||
'w-full my-auto rounded-xl max-md:hidden',
|
||||
navMode === 'top' ? 'max-w-sm px-2' : '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
|
||||
@@ -288,8 +300,12 @@ function DocsNavbar({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{nav.children}
|
||||
<SearchToggle hideIfDisabled className="md:hidden" />
|
||||
{children}
|
||||
{slots(
|
||||
'sm',
|
||||
searchToggle,
|
||||
<SearchToggle hideIfDisabled className="md:hidden" />,
|
||||
)}
|
||||
<NavbarSidebarTrigger className="md:hidden" />
|
||||
{links
|
||||
.filter((item) => item.type === 'icon')
|
||||
@@ -311,7 +327,7 @@ function DocsNavbar({
|
||||
<Languages className="size-4.5 text-fd-muted-foreground" />
|
||||
</LanguageToggle>
|
||||
) : null}
|
||||
{replaceOrDefault(
|
||||
{slot(
|
||||
themeSwitch,
|
||||
<ThemeToggle
|
||||
className="ms-2 max-md:hidden"
|
||||
|
||||
@@ -16,12 +16,6 @@ export interface NavOptions extends NavProviderProps {
|
||||
*/
|
||||
url?: string;
|
||||
|
||||
/**
|
||||
* Show/hide search toggle
|
||||
*
|
||||
* Note: Enable/disable search from root provider instead
|
||||
*/
|
||||
enableSearch?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
@@ -32,6 +26,14 @@ export interface BaseLayoutProps {
|
||||
mode?: 'light-dark' | 'light-dark-system';
|
||||
};
|
||||
|
||||
searchToggle?: Partial<{
|
||||
enabled: boolean;
|
||||
components: Partial<{
|
||||
sm: ReactNode;
|
||||
lg: ReactNode;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Remove theme switcher component
|
||||
*
|
||||
@@ -91,7 +93,7 @@ export function getLinks(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function replaceOrDefault(
|
||||
export function slot(
|
||||
obj:
|
||||
| {
|
||||
enabled?: boolean;
|
||||
@@ -108,3 +110,20 @@ export function replaceOrDefault(
|
||||
|
||||
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 <Slot>{obj.components[variant]}</Slot>;
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { type AnchorProviderProps, AnchorProvider } from 'fumadocs-core/toc';
|
||||
import { replaceOrDefault } from '@/layouts/shared';
|
||||
import { slot } from '@/layouts/shared';
|
||||
import { cn } from './utils/cn';
|
||||
import {
|
||||
Footer,
|
||||
@@ -151,7 +151,7 @@ export function DocsPage({
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{replaceOrDefault(
|
||||
{slot(
|
||||
{ enabled: tocPopoverEnabled, component: tocPopoverReplace },
|
||||
<TocPopoverHeader className="h-10">
|
||||
<TocPopoverTrigger className="w-full" items={toc} />
|
||||
@@ -179,10 +179,7 @@ export function DocsPage({
|
||||
props.article?.className,
|
||||
)}
|
||||
>
|
||||
{replaceOrDefault(
|
||||
props.breadcrumb,
|
||||
<Breadcrumb {...props.breadcrumb} />,
|
||||
)}
|
||||
{slot(props.breadcrumb, <Breadcrumb {...props.breadcrumb} />)}
|
||||
{props.children}
|
||||
<div role="none" className="flex-1" />
|
||||
<div className="flex flex-row flex-wrap items-center justify-between gap-4 empty:hidden">
|
||||
@@ -193,13 +190,10 @@ export function DocsPage({
|
||||
<LastUpdate date={new Date(props.lastUpdate)} />
|
||||
) : null}
|
||||
</div>
|
||||
{replaceOrDefault(
|
||||
props.footer,
|
||||
<Footer items={props.footer?.items} />,
|
||||
)}
|
||||
{slot(props.footer, <Footer items={props.footer?.items} />)}
|
||||
</PageArticle>
|
||||
</PageBody>
|
||||
{replaceOrDefault(
|
||||
{slot(
|
||||
{ enabled: tocEnabled, component: tocReplace },
|
||||
<Toc>
|
||||
{tocOptions.header}
|
||||
|
||||
Reference in New Issue
Block a user