mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
UI: clean code
This commit is contained in:
@@ -19,6 +19,17 @@ export const registry: Registry = {
|
||||
},
|
||||
components: [
|
||||
...commonComponents,
|
||||
{
|
||||
name: 'link-item',
|
||||
unlisted: true,
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
path: 'utils/link-item.tsx',
|
||||
target: '<dir>/layout/link-item.tsx',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'layouts/shared',
|
||||
unlisted: true,
|
||||
|
||||
@@ -4,11 +4,10 @@ import { type ComponentProps, createContext, type ReactNode, use, useMemo } from
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useSidebar } from '@/components/sidebar/base';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
import { isTabActive } from '@/components/sidebar/tabs/dropdown';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import type { SidebarTab } from '@/components/sidebar/tabs';
|
||||
import { isTabActive } from '@/components/sidebar/tabs/dropdown';
|
||||
import { useIsScrollTop } from '@/utils/use-is-scroll-top';
|
||||
import { renderer, type Renderer } from '@/utils/renderer';
|
||||
|
||||
export const LayoutContext = createContext<{
|
||||
isNavTransparent: boolean;
|
||||
@@ -57,38 +56,33 @@ export function LayoutHeader(props: ComponentProps<'header'>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutBody({
|
||||
_,
|
||||
children,
|
||||
}: {
|
||||
_: Renderer<ComponentProps<'div'>>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const render = renderer(_, 'div');
|
||||
export function LayoutBody({ className, style, children, ...props }: ComponentProps<'div'>) {
|
||||
const { collapsed } = useSidebar();
|
||||
|
||||
return render?.(
|
||||
(t) =>
|
||||
({
|
||||
id: 'nd-docs-layout',
|
||||
'data-sidebar-collapsed': collapsed,
|
||||
children,
|
||||
...t,
|
||||
className: cn(
|
||||
'grid transition-[grid-template-columns] overflow-x-clip min-h-(--fd-docs-height) [--fd-docs-height:100dvh] [--fd-header-height:0px] [--fd-toc-popover-height:0px] [--fd-sidebar-width:0px] [--fd-toc-width:0px]',
|
||||
t?.className,
|
||||
),
|
||||
style: {
|
||||
return (
|
||||
<div
|
||||
id="nd-docs-layout"
|
||||
className={cn(
|
||||
'grid transition-[grid-template-columns] overflow-x-clip min-h-(--fd-docs-height) [--fd-docs-height:100dvh] [--fd-header-height:0px] [--fd-toc-popover-height:0px] [--fd-sidebar-width:0px] [--fd-toc-width:0px]',
|
||||
className,
|
||||
)}
|
||||
data-sidebar-collapsed={collapsed}
|
||||
style={
|
||||
{
|
||||
gridTemplate: `"sidebar sidebar header toc toc"
|
||||
"sidebar sidebar toc-popover toc toc"
|
||||
"sidebar sidebar main toc toc" 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-width) - var(--fd-toc-width))) var(--fd-toc-width) minmax(min-content, 1fr)`,
|
||||
"sidebar sidebar toc-popover toc toc"
|
||||
"sidebar sidebar main toc toc" 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-width) - var(--fd-toc-width))) var(--fd-toc-width) minmax(min-content, 1fr)`,
|
||||
'--fd-docs-row-1': 'var(--fd-banner-height, 0px)',
|
||||
'--fd-docs-row-2': 'calc(var(--fd-docs-row-1) + var(--fd-header-height))',
|
||||
'--fd-docs-row-3': 'calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height))',
|
||||
'--fd-sidebar-col': collapsed ? '0px' : 'var(--fd-sidebar-width)',
|
||||
...t?.style,
|
||||
},
|
||||
}) as ComponentProps<'div'>,
|
||||
...style,
|
||||
} as object
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,7 @@ import {
|
||||
SidebarTrigger,
|
||||
SidebarViewport,
|
||||
} from './sidebar';
|
||||
import {
|
||||
type BaseLayoutProps,
|
||||
parseLayoutProps,
|
||||
renderTitleNav,
|
||||
useLinkItems,
|
||||
} from '@/layouts/shared';
|
||||
import { type BaseLayoutProps, renderTitleNav, useLinkItems } from '@/layouts/shared';
|
||||
import { LinkItem } from '@/utils/link-item';
|
||||
import { LanguageToggle, LanguageToggleText } from '@/layouts/shared/language-toggle';
|
||||
import { LayoutBody, LayoutContextProvider, LayoutHeader, LayoutTabs } from './client';
|
||||
@@ -28,18 +23,16 @@ import { LargeSearchToggle, SearchToggle } from '@/layouts/shared/search-toggle'
|
||||
import { getSidebarTabs, type GetSidebarTabsOptions } from '@/components/sidebar/tabs';
|
||||
import type { SidebarPageTreeComponents } from '@/components/sidebar/page-tree';
|
||||
import { SidebarTabsDropdown, type SidebarTabWithProps } from '@/components/sidebar/tabs/dropdown';
|
||||
import { type Renderer, renderer } from '@/utils/renderer';
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
|
||||
sidebar?: SidebarOptions;
|
||||
|
||||
tabMode?: 'top' | 'auto';
|
||||
SidebarTrigger?: Renderer<ComponentProps<'button'>>;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
|
||||
/**
|
||||
* @deprecated use `Container` instead.
|
||||
* Props for the `div` container
|
||||
*/
|
||||
containerProps?: HTMLAttributes<HTMLDivElement>;
|
||||
}
|
||||
@@ -68,35 +61,23 @@ interface SidebarOptions
|
||||
collapsible?: boolean;
|
||||
}
|
||||
|
||||
export function DocsLayout(_: DocsLayoutProps) {
|
||||
const {
|
||||
nav: { transparentMode, ...nav } = {},
|
||||
sidebar: {
|
||||
tabs: sidebarTabs,
|
||||
enabled: sidebarEnabled = true,
|
||||
defaultOpenLevel,
|
||||
prefetch,
|
||||
...sidebarProps
|
||||
} = {},
|
||||
SearchToggle: SearchToggleRenderer,
|
||||
LargeSearchToggle: LargeSearchToggleRenderer,
|
||||
ThemeSwitch,
|
||||
containerProps,
|
||||
Container = containerProps ?? true,
|
||||
tabMode = 'auto',
|
||||
LanguageSwitch,
|
||||
SidebarTrigger: SidebarTriggerRenderer = sidebarEnabled,
|
||||
children,
|
||||
tree,
|
||||
...props
|
||||
} = parseLayoutProps<DocsLayoutProps>(_);
|
||||
|
||||
const renderSearchToggle = renderer(SearchToggleRenderer, SearchToggle);
|
||||
const renderLargeSearchToggle = renderer(LargeSearchToggleRenderer, LargeSearchToggle);
|
||||
const renderThemeSwitch = renderer(ThemeSwitch, ThemeToggle);
|
||||
const renderLanguageSwitch = renderer(LanguageSwitch, LanguageToggle);
|
||||
const renderSidebarTrigger = renderer(SidebarTriggerRenderer, SidebarTrigger);
|
||||
|
||||
export function DocsLayout({
|
||||
nav: { transparentMode, ...nav } = {},
|
||||
sidebar: {
|
||||
tabs: sidebarTabs,
|
||||
enabled: sidebarEnabled = true,
|
||||
defaultOpenLevel,
|
||||
prefetch,
|
||||
...sidebarProps
|
||||
} = {},
|
||||
searchToggle = {},
|
||||
themeSwitch = {},
|
||||
tabMode = 'auto',
|
||||
i18n = false,
|
||||
children,
|
||||
tree,
|
||||
...props
|
||||
}: DocsLayoutProps) {
|
||||
const tabs = useMemo(() => {
|
||||
if (Array.isArray(sidebarTabs)) {
|
||||
return sidebarTabs;
|
||||
@@ -150,21 +131,20 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
</SidebarCollapseTrigger>
|
||||
)}
|
||||
</div>
|
||||
{renderLargeSearchToggle?.((o) => ({
|
||||
hideIfDisabled: true,
|
||||
...o,
|
||||
}))}
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.lg ?? <LargeSearchToggle hideIfDisabled />)}
|
||||
{tabs.length > 0 && tabMode === 'auto' && <SidebarTabsDropdown options={tabs} />}
|
||||
{banner}
|
||||
</div>
|
||||
{viewport}
|
||||
{(renderLanguageSwitch || iconLinks.length > 0 || renderThemeSwitch || footer) && (
|
||||
{(i18n || iconLinks.length > 0 || themeSwitch?.enabled !== false || footer) && (
|
||||
<div className="flex flex-col border-t p-4 pt-2 empty:hidden">
|
||||
<div className="flex text-fd-muted-foreground items-center empty:hidden">
|
||||
{renderLanguageSwitch?.((o) => ({
|
||||
children: <Languages className="size-4.5" />,
|
||||
...o,
|
||||
}))}
|
||||
{i18n && (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-4.5" />
|
||||
</LanguageToggle>
|
||||
)}
|
||||
{iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
@@ -175,7 +155,10 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
))}
|
||||
{renderThemeSwitch?.((t) => ({ ...t, className: cn('ms-auto p-0', t?.className) }))}
|
||||
{themeSwitch.enabled !== false &&
|
||||
(themeSwitch.component ?? (
|
||||
<ThemeToggle className="ms-auto p-0" mode={themeSwitch.mode} />
|
||||
))}
|
||||
</div>
|
||||
{footer}
|
||||
</div>
|
||||
@@ -202,28 +185,25 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
</LinkItem>
|
||||
))}
|
||||
</div>
|
||||
{renderLanguageSwitch?.((o) => ({
|
||||
children: (
|
||||
<>
|
||||
<Languages className="size-4.5" />
|
||||
<LanguageToggleText />
|
||||
</>
|
||||
),
|
||||
...o,
|
||||
}))}
|
||||
{renderThemeSwitch?.((t) => ({ ...t, className: cn('p-0', t?.className) }))}
|
||||
{renderSidebarTrigger?.((t) => ({
|
||||
children: <SidebarIcon />,
|
||||
...t,
|
||||
className: cn(
|
||||
{i18n && (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-4.5" />
|
||||
<LanguageToggleText />
|
||||
</LanguageToggle>
|
||||
)}
|
||||
{themeSwitch.enabled !== false &&
|
||||
(themeSwitch.component ?? <ThemeToggle className="p-0" mode={themeSwitch.mode} />)}
|
||||
<SidebarTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'p-2',
|
||||
}),
|
||||
'p-2',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarTrigger>
|
||||
</div>
|
||||
{tabs.length > 0 && <SidebarTabsDropdown options={tabs} />}
|
||||
{banner}
|
||||
@@ -239,7 +219,7 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
<TreeContextProvider tree={tree}>
|
||||
<LayoutContextProvider navTransparentMode={transparentMode}>
|
||||
<Sidebar defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
|
||||
<LayoutBody _={Container}>
|
||||
<LayoutBody {...props.containerProps}>
|
||||
{nav.enabled !== false &&
|
||||
(nav.component ?? (
|
||||
<LayoutHeader
|
||||
@@ -250,23 +230,23 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
className: 'inline-flex items-center gap-2.5 font-semibold',
|
||||
})}
|
||||
<div className="flex-1">{nav.children}</div>
|
||||
{renderSearchToggle?.((o) => ({
|
||||
hideIfDisabled: true,
|
||||
...o,
|
||||
className: cn('p-2', o?.className),
|
||||
}))}
|
||||
{renderSidebarTrigger?.((t) => ({
|
||||
children: <SidebarIcon />,
|
||||
...t,
|
||||
className: cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
}),
|
||||
'p-2',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.sm ?? (
|
||||
<SearchToggle className="p-2" hideIfDisabled />
|
||||
))}
|
||||
{sidebarEnabled && (
|
||||
<SidebarTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'p-2',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarTrigger>
|
||||
)}
|
||||
</LayoutHeader>
|
||||
))}
|
||||
{sidebarEnabled && sidebar()}
|
||||
|
||||
@@ -16,132 +16,157 @@ import type { AnchorProviderProps, TOCItemType } from 'fumadocs-core/toc';
|
||||
import * as TocDefault from '@/components/toc/default';
|
||||
import * as TocClerk from '@/components/toc/clerk';
|
||||
import { TOCProvider, TOCScrollArea } from '@/components/toc';
|
||||
import { ChildrenRenderer, renderer, type Renderer } from '@/utils/renderer';
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
export interface DocsPageProps {
|
||||
toc?: TOCItemType[];
|
||||
/**
|
||||
* - `multiple` (default): Accept multiple active items
|
||||
* - `single`: Only accept one active item at most
|
||||
* */
|
||||
tocMode?: 'single' | 'multiple';
|
||||
tableOfContent?: Partial<TableOfContentOptions>;
|
||||
tableOfContentPopover?: Partial<TableOfContentPopoverOptions>;
|
||||
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
|
||||
/**
|
||||
* Replace or disable breadcrumb
|
||||
*/
|
||||
breadcrumb?: Partial<BreadcrumbOptions>;
|
||||
|
||||
/**
|
||||
* Footer navigation, located under the page body.
|
||||
*
|
||||
* You can specify `footer.children` to add extra components under the footer.
|
||||
*/
|
||||
footer?: Partial<FooterOptions>;
|
||||
|
||||
children?: ReactNode;
|
||||
|
||||
TOC?: Renderer<TOCMainProps>;
|
||||
TOCPopover?: Renderer<TOCPopoverProps>;
|
||||
/** Footer navigation, located under the page body. */
|
||||
Footer?: Renderer<FooterProps>;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
Breadcrumb?: Renderer<BreadcrumbProps>;
|
||||
|
||||
/** @deprecated use `Footer` instead. */
|
||||
footer?: FooterOptions;
|
||||
/** @deprecated use `Container` instead. */
|
||||
/**
|
||||
* Apply class names to the `#nd-page` container.
|
||||
*/
|
||||
className?: string;
|
||||
/** @deprecated use `Breadcrumb` instead. */
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
/** @deprecated use `TOC` instead, or `tocMode` for enabling `single`. */
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
/** @deprecated use `TOCPopover` instead. */
|
||||
tableOfContentPopover?: TableOfContentPopoverOptions;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
type TableOfContentOptions = Pick<AnchorProviderProps, 'single'> & {
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
header?: ReactNode;
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
/**
|
||||
* Custom content in TOC container, after the main TOC
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
|
||||
interface TableOfContentOptions extends Pick<AnchorProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
|
||||
interface TableOfContentPopoverOptions extends TOCProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
style?: 'normal' | 'clerk';
|
||||
};
|
||||
|
||||
type TableOfContentPopoverOptions = Omit<TableOfContentOptions, 'single'>;
|
||||
|
||||
export function DocsPage({
|
||||
tableOfContent: tocProps = {},
|
||||
TOC: TOCRenderer,
|
||||
tableOfContentPopover: tocPopoverProps = {},
|
||||
TOCPopover: TOCPopoverRenderer,
|
||||
footer = {},
|
||||
Footer = footer.enabled === false
|
||||
? false
|
||||
: footer.component
|
||||
? new ChildrenRenderer(footer.component)
|
||||
: footer,
|
||||
className,
|
||||
Container = { className },
|
||||
breadcrumb = {},
|
||||
Breadcrumb = breadcrumb.enabled === false
|
||||
? false
|
||||
: breadcrumb.component
|
||||
? new ChildrenRenderer(breadcrumb.component)
|
||||
: breadcrumb,
|
||||
breadcrumb: { enabled: breadcrumbEnabled = true, component: breadcrumb, ...breadcrumbProps } = {},
|
||||
footer: { enabled: footerEnabled, component: footerReplace, ...footerProps } = {},
|
||||
full = false,
|
||||
tableOfContentPopover: {
|
||||
enabled: tocPopoverEnabled,
|
||||
component: tocPopover,
|
||||
...tocPopoverOptions
|
||||
} = {},
|
||||
tableOfContent: { enabled: tocEnabled, component: tocReplace, ...tocOptions } = {},
|
||||
toc = [],
|
||||
tocMode = tocProps.single ? 'single' : 'multiple',
|
||||
children,
|
||||
className,
|
||||
}: DocsPageProps) {
|
||||
if (!full && (tocProps.enabled ?? (toc.length > 0 || tocProps.footer || tocProps.header))) {
|
||||
TOCRenderer ??= tocProps.component ? new ChildrenRenderer(tocProps.component) : tocProps;
|
||||
} else {
|
||||
// force disable toc in full mode or when no content
|
||||
TOCRenderer = false;
|
||||
// disable TOC on full mode, you can still enable it with `enabled` option.
|
||||
tocEnabled ??=
|
||||
!full && (toc.length > 0 || tocOptions.footer !== undefined || tocOptions.header !== undefined);
|
||||
|
||||
tocPopoverEnabled ??=
|
||||
toc.length > 0 ||
|
||||
tocPopoverOptions.header !== undefined ||
|
||||
tocPopoverOptions.footer !== undefined;
|
||||
|
||||
let wrapper = (children: ReactNode) => children;
|
||||
|
||||
if (tocEnabled || tocPopoverEnabled) {
|
||||
wrapper = (children) => (
|
||||
<TOCProvider single={tocOptions.single} toc={toc}>
|
||||
{children}
|
||||
</TOCProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
tocPopoverProps.enabled ??
|
||||
(toc.length > 0 || tocPopoverProps.header || tocPopoverProps.footer)
|
||||
) {
|
||||
TOCPopoverRenderer ??= tocPopoverProps.component
|
||||
? new ChildrenRenderer(tocPopoverProps.component)
|
||||
: tocPopoverProps;
|
||||
} else {
|
||||
TOCPopoverRenderer = false;
|
||||
}
|
||||
|
||||
const renderBreadcrumb = renderer(Breadcrumb, PageBreadcrumb);
|
||||
const renderFooter = renderer(Footer, PageFooter);
|
||||
const renderContainer = renderer(Container, 'article');
|
||||
const renderToc = renderer(TOCRenderer, TOC);
|
||||
const renderTocPopover = renderer(TOCPopoverRenderer, TOCPopover);
|
||||
|
||||
return (
|
||||
<TOCProvider single={tocMode === 'single'} toc={renderToc || renderTocPopover ? toc : []}>
|
||||
{renderTocPopover?.((t) => t ?? {})}
|
||||
{renderContainer?.((t) => ({
|
||||
id: 'nd-page',
|
||||
'data-full': full,
|
||||
children: (
|
||||
<>
|
||||
{renderBreadcrumb?.((t) => t ?? {})}
|
||||
{children}
|
||||
{renderFooter?.((t) => t ?? {})}
|
||||
</>
|
||||
),
|
||||
...t,
|
||||
className: cn(
|
||||
return wrapper(
|
||||
<>
|
||||
{tocPopoverEnabled &&
|
||||
(tocPopover ?? (
|
||||
<PageTOCPopover>
|
||||
<PageTOCPopoverTrigger />
|
||||
<PageTOCPopoverContent>
|
||||
{tocPopoverOptions.header}
|
||||
<TOCScrollArea>
|
||||
{tocPopoverOptions.style === 'clerk' ? (
|
||||
<TocClerk.TOCItems />
|
||||
) : (
|
||||
<TocDefault.TOCItems />
|
||||
)}
|
||||
</TOCScrollArea>
|
||||
{tocPopoverOptions.footer}
|
||||
</PageTOCPopoverContent>
|
||||
</PageTOCPopover>
|
||||
))}
|
||||
<article
|
||||
id="nd-page"
|
||||
data-full={full}
|
||||
className={cn(
|
||||
'flex flex-col w-full max-w-[900px] mx-auto [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14',
|
||||
full ? 'max-w-[1168px]' : 'xl:layout:[--fd-toc-width:268px]',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
{renderToc?.((t) => t ?? {})}
|
||||
</TOCProvider>
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{breadcrumbEnabled && (breadcrumb ?? <PageBreadcrumb {...breadcrumbProps} />)}
|
||||
{children}
|
||||
{footerEnabled !== false && (footerReplace ?? <PageFooter {...footerProps} />)}
|
||||
</article>
|
||||
{tocEnabled &&
|
||||
(tocReplace ?? (
|
||||
<div
|
||||
id="nd-toc"
|
||||
className="sticky top-(--fd-docs-row-1) h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))] flex flex-col [grid-area:toc] w-(--fd-toc-width) pt-12 pe-4 pb-2 max-xl:hidden"
|
||||
>
|
||||
{tocOptions.header}
|
||||
<h3
|
||||
id="toc-title"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground"
|
||||
>
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
{tocOptions.style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{tocOptions.footer}
|
||||
</div>
|
||||
))}
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,86 +225,5 @@ export function DocsTitle({ children, className, ...props }: ComponentProps<'h1'
|
||||
);
|
||||
}
|
||||
|
||||
export interface TOCPopoverProps extends TOCProps {
|
||||
container?: ComponentProps<typeof PageTOCPopover>;
|
||||
trigger?: ComponentProps<typeof PageTOCPopoverTrigger>;
|
||||
content?: ComponentProps<typeof PageTOCPopoverContent>;
|
||||
}
|
||||
|
||||
export interface TOCMainProps extends TOCProps {
|
||||
container?: ComponentProps<typeof PageTOCPopover>;
|
||||
}
|
||||
|
||||
interface TOCProps {
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
header?: ReactNode;
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, after the main TOC
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
style?: 'normal' | 'clerk';
|
||||
}
|
||||
|
||||
export function TOC({ container, header, footer, style }: TOCMainProps) {
|
||||
return (
|
||||
<div
|
||||
id="nd-toc"
|
||||
{...container}
|
||||
className={cn(
|
||||
'sticky top-(--fd-docs-row-1) h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))] flex flex-col [grid-area:toc] w-(--fd-toc-width) pt-12 pe-4 pb-2 max-xl:hidden',
|
||||
container?.className,
|
||||
)}
|
||||
>
|
||||
{header}
|
||||
<h3
|
||||
id="toc-title"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground"
|
||||
>
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TOCPopover({
|
||||
container,
|
||||
trigger,
|
||||
content,
|
||||
header,
|
||||
footer,
|
||||
style,
|
||||
}: TOCPopoverProps) {
|
||||
return (
|
||||
<PageTOCPopover {...container}>
|
||||
<PageTOCPopoverTrigger {...trigger} />
|
||||
<PageTOCPopoverContent {...content}>
|
||||
{header}
|
||||
<TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{footer}
|
||||
</PageTOCPopoverContent>
|
||||
</PageTOCPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
PageLastUpdate,
|
||||
PageBreadcrumb,
|
||||
PageFooter,
|
||||
type BreadcrumbProps,
|
||||
type FooterProps,
|
||||
} from './client';
|
||||
export { PageLastUpdate, PageBreadcrumb } from './client';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/shared/page-actions';
|
||||
|
||||
@@ -4,14 +4,14 @@ import type { BaseLayoutProps } from '@/layouts/shared';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { getSidebarTabs, type GetSidebarTabsOptions } from '@/components/sidebar/tabs';
|
||||
import type { SidebarPageTreeComponents } from '@/components/sidebar/page-tree';
|
||||
import { type ComponentProps, type ReactNode, useMemo } from 'react';
|
||||
import { type ComponentProps, HTMLAttributes, type ReactNode, useMemo } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useSidebar } from '@/components/sidebar/base';
|
||||
import { SidebarViewport, useSidebar } from '@/components/sidebar/base';
|
||||
import { SidebarTabsDropdown, type SidebarTabWithProps } from './tab-dropdown';
|
||||
import { Sidebar, SidebarContent, SidebarLinkItem, SidebarPageTree } from './sidebar';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Languages, SidebarIcon, XIcon } from 'lucide-react';
|
||||
import { parseLayoutProps, renderTitleNav, useLinkItems } from '../shared';
|
||||
import { renderTitleNav, useLinkItems } from '../shared';
|
||||
import { LanguageToggle } from '../shared/language-toggle';
|
||||
import { SearchToggle } from '../shared/search-toggle';
|
||||
import { ThemeToggle } from '../shared/theme-toggle';
|
||||
@@ -19,19 +19,18 @@ import { LinkItem } from '@/utils/link-item';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { RemoveScroll } from 'react-remove-scroll';
|
||||
import { useSearchContext } from '@/contexts/search';
|
||||
import { ScrollArea, ScrollViewport } from '@/components/ui/scroll-area';
|
||||
import { renderer, type Renderer } from '@/utils/renderer';
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
|
||||
sidebar?: SidebarOptions;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
renderNavigationPanel?: (props: NavigationPanelProps) => ReactNode;
|
||||
|
||||
/**
|
||||
* @deprecated use `Container` instead.
|
||||
* Props for the `div` container
|
||||
*/
|
||||
containerProps?: ComponentProps<'div'>;
|
||||
containerProps?: HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
renderNavigationPanel?: (props: NavigationPanelProps) => ReactNode;
|
||||
}
|
||||
|
||||
interface SidebarOptions
|
||||
@@ -51,32 +50,24 @@ interface SidebarOptions
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
export function DocsLayout(_: DocsLayoutProps) {
|
||||
const {
|
||||
tree,
|
||||
nav = {},
|
||||
sidebar: {
|
||||
enabled: sidebarEnabled = true,
|
||||
tabs: sidebarTabs,
|
||||
defaultOpenLevel,
|
||||
prefetch,
|
||||
...sidebarProps
|
||||
} = {},
|
||||
children,
|
||||
containerProps,
|
||||
Container = containerProps ?? true,
|
||||
LanguageSwitch,
|
||||
SearchToggle: SearchToggleRenderer,
|
||||
ThemeSwitch,
|
||||
renderNavigationPanel = (props) => <NavigationPanel {...props} />,
|
||||
...props
|
||||
} = parseLayoutProps<DocsLayoutProps>(_);
|
||||
const { menuItems } = useLinkItems(props);
|
||||
const renderContainer = renderer(Container, 'div');
|
||||
const renderLanguageSwitch = renderer(LanguageSwitch, LanguageToggle);
|
||||
const renderThemeSwitch = renderer(ThemeSwitch, ThemeToggle);
|
||||
const renderSearchToggle = renderer(SearchToggleRenderer, SearchToggle);
|
||||
|
||||
export function DocsLayout({
|
||||
tree,
|
||||
nav = {},
|
||||
sidebar: {
|
||||
enabled: sidebarEnabled = true,
|
||||
tabs: sidebarTabs,
|
||||
defaultOpenLevel,
|
||||
prefetch,
|
||||
...sidebarProps
|
||||
} = {},
|
||||
searchToggle = {},
|
||||
themeSwitch = {},
|
||||
i18n = false,
|
||||
children,
|
||||
containerProps,
|
||||
renderNavigationPanel = (props) => <NavigationPanel {...props} />,
|
||||
...props
|
||||
}: DocsLayoutProps) {
|
||||
const tabs = useMemo(() => {
|
||||
if (Array.isArray(sidebarTabs)) {
|
||||
return sidebarTabs;
|
||||
@@ -89,6 +80,7 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
}
|
||||
return [];
|
||||
}, [tree, sidebarTabs]);
|
||||
const { menuItems } = useLinkItems(props);
|
||||
const iconLinks = menuItems.filter((item) => item.type === 'icon');
|
||||
|
||||
function sidebar() {
|
||||
@@ -98,28 +90,18 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
return (
|
||||
<SidebarContent {...rest}>
|
||||
<div className="flex flex-col gap-3 p-4 pb-2 empty:hidden">{banner}</div>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<ScrollViewport
|
||||
className="p-4 overscroll-contain"
|
||||
style={
|
||||
{
|
||||
maskImage:
|
||||
'linear-gradient(to bottom, transparent, white 12px, white calc(100% - 12px), transparent)',
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{menuItems
|
||||
.filter((v) => v.type !== 'icon')
|
||||
.map((item, i, list) => (
|
||||
<SidebarLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(i === list.length - 1 && 'mb-4')}
|
||||
/>
|
||||
))}
|
||||
<SidebarPageTree {...components} />
|
||||
</ScrollViewport>
|
||||
</ScrollArea>
|
||||
<SidebarViewport>
|
||||
{menuItems
|
||||
.filter((v) => v.type !== 'icon')
|
||||
.map((item, i, list) => (
|
||||
<SidebarLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(i === list.length - 1 && 'mb-4')}
|
||||
/>
|
||||
))}
|
||||
<SidebarPageTree {...components} />
|
||||
</SidebarViewport>
|
||||
{footer}
|
||||
</SidebarContent>
|
||||
);
|
||||
@@ -128,17 +110,17 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
return (
|
||||
<TreeContextProvider tree={tree}>
|
||||
<Sidebar defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
|
||||
{renderContainer?.((t) => ({
|
||||
id: 'nd-flux-layout',
|
||||
children: (
|
||||
<>
|
||||
{sidebarEnabled && sidebar()}
|
||||
{children}
|
||||
</>
|
||||
),
|
||||
...t,
|
||||
className: cn('flex flex-col items-center pb-24 overflow-x-clip', t?.className),
|
||||
}))}
|
||||
<div
|
||||
id="nd-flux-layout"
|
||||
{...containerProps}
|
||||
className={cn(
|
||||
'flex flex-col items-center pb-24 overflow-x-clip',
|
||||
containerProps?.className,
|
||||
)}
|
||||
>
|
||||
{sidebarEnabled && sidebar()}
|
||||
{children}
|
||||
</div>
|
||||
{renderNavigationPanel({
|
||||
head: renderTitleNav(nav, {
|
||||
className: 'inline-flex items-center gap-2.5 text-sm font-semibold',
|
||||
@@ -146,21 +128,25 @@ export function DocsLayout(_: DocsLayoutProps) {
|
||||
tabDropdown: tabs.length > 0 && <SidebarTabsDropdown className="flex-1" options={tabs} />,
|
||||
tool: (
|
||||
<>
|
||||
{renderLanguageSwitch?.((t) => ({
|
||||
children: <Languages className="size-4.5" />,
|
||||
...t,
|
||||
}))}
|
||||
{renderSearchToggle?.((t) => ({
|
||||
hideIfDisabled: true,
|
||||
...t,
|
||||
className: cn('rounded-lg', t?.className),
|
||||
}))}
|
||||
{i18n && (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-4.5" />
|
||||
</LanguageToggle>
|
||||
)}
|
||||
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.sm ?? (
|
||||
<SearchToggle className="rounded-lg" hideIfDisabled />
|
||||
))}
|
||||
|
||||
<NavigationSidebarTrigger />
|
||||
{renderThemeSwitch?.((t) => ({
|
||||
...t,
|
||||
className: cn('p-1 h-full ms-1 rounded-xl bg-fd-muted *:rounded-lg', t?.className),
|
||||
}))}
|
||||
{themeSwitch.enabled !== false &&
|
||||
(themeSwitch.component ?? (
|
||||
<ThemeToggle
|
||||
className="px-1 py-0 h-full ms-1 rounded-xl bg-fd-muted *:rounded-lg"
|
||||
mode={themeSwitch.mode}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
link: iconLinks.map((item, i) => (
|
||||
|
||||
@@ -16,20 +16,20 @@ import type { AnchorProviderProps, TOCItemType } from 'fumadocs-core/toc';
|
||||
import * as TocDefault from '@/components/toc/default';
|
||||
import * as TocClerk from '@/components/toc/clerk';
|
||||
import { TOCProvider, TOCScrollArea } from '@/components/toc';
|
||||
import { ChildrenRenderer, renderer, type Renderer } from '@/utils/renderer';
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
export interface DocsPageProps {
|
||||
toc?: TOCItemType[];
|
||||
/**
|
||||
* - `multiple` (default): Accept multiple active items
|
||||
* - `single`: Only accept one active item at most
|
||||
* */
|
||||
tocMode?: 'single' | 'multiple';
|
||||
TOC?: Renderer<TOCProps>;
|
||||
/** Footer navigation, located under the page body. */
|
||||
Footer?: Renderer<FooterProps>;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
Breadcrumb?: Renderer<BreadcrumbProps>;
|
||||
tableOfContent?: Partial<TableOfContentOptions>;
|
||||
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
@@ -37,38 +37,28 @@ export interface DocsPageProps {
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
|
||||
/**
|
||||
* Replace or disable breadcrumb
|
||||
*/
|
||||
breadcrumb?: Partial<BreadcrumbOptions>;
|
||||
|
||||
/**
|
||||
* Footer navigation, located under the page body.
|
||||
*
|
||||
* You can specify `footer.children` to add extra components under the footer.
|
||||
*/
|
||||
footer?: Partial<FooterOptions>;
|
||||
|
||||
children?: ReactNode;
|
||||
|
||||
/** @deprecated use `Footer` instead. */
|
||||
footer?: FooterOptions;
|
||||
/** @deprecated use `Container` instead. */
|
||||
/**
|
||||
* Apply class names to the `#nd-page` container.
|
||||
*/
|
||||
className?: string;
|
||||
/** @deprecated use `Breadcrumb` instead. */
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
/** @deprecated use `TOC` instead, or `tocMode` for enabling `single`. */
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
}
|
||||
|
||||
interface TableOfContentOptions extends Pick<AnchorProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
export interface TOCProps {
|
||||
container?: ComponentProps<typeof PageTOCPopover>;
|
||||
trigger?: ComponentProps<typeof PageTOCPopoverTrigger>;
|
||||
content?: ComponentProps<typeof PageTOCPopoverContent>;
|
||||
|
||||
interface TableOfContentOptions extends Pick<AnchorProviderProps, 'single'> {
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
@@ -79,6 +69,9 @@ export interface TOCProps {
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
@@ -86,59 +79,56 @@ export interface TOCProps {
|
||||
}
|
||||
|
||||
export function DocsPage({
|
||||
tableOfContent: tocProps = {},
|
||||
TOC: TOCRenderer,
|
||||
footer = {},
|
||||
Footer = footer.enabled === false
|
||||
? false
|
||||
: footer.component
|
||||
? new ChildrenRenderer(footer.component)
|
||||
: footer,
|
||||
className,
|
||||
Container = { className },
|
||||
breadcrumb = {},
|
||||
Breadcrumb = breadcrumb.enabled === false
|
||||
? false
|
||||
: breadcrumb.component
|
||||
? new ChildrenRenderer(breadcrumb.component)
|
||||
: breadcrumb,
|
||||
breadcrumb: { enabled: breadcrumbEnabled = true, component: breadcrumb, ...breadcrumbProps } = {},
|
||||
footer: { enabled: footerEnabled, component: footerReplace, ...footerProps } = {},
|
||||
full = false,
|
||||
tableOfContent: { enabled: tocPopoverEnabled, component: tocPopover, ...tocOptions } = {},
|
||||
toc = [],
|
||||
tocMode = tocProps.single ? 'single' : 'multiple',
|
||||
children,
|
||||
className,
|
||||
}: DocsPageProps) {
|
||||
if (tocProps.enabled ?? (toc.length > 0 || tocProps.header || tocProps.footer)) {
|
||||
TOCRenderer ??= tocProps.component ? new ChildrenRenderer(tocProps.component) : tocProps;
|
||||
} else {
|
||||
TOCRenderer = false;
|
||||
tocPopoverEnabled ??=
|
||||
toc.length > 0 || tocOptions.header !== undefined || tocOptions.footer !== undefined;
|
||||
|
||||
let wrapper = (children: ReactNode) => children;
|
||||
|
||||
if (tocPopoverEnabled) {
|
||||
wrapper = (children) => (
|
||||
<TOCProvider single={tocOptions.single} toc={toc}>
|
||||
{children}
|
||||
</TOCProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const renderBreadcrumb = renderer(Breadcrumb, PageBreadcrumb);
|
||||
const renderFooter = renderer(Footer, PageFooter);
|
||||
const renderContainer = renderer(Container, 'article');
|
||||
const renderToc = renderer(TOCRenderer, TOC);
|
||||
|
||||
return (
|
||||
<TOCProvider single={tocMode === 'single'} toc={renderToc ? toc : []}>
|
||||
{renderToc?.((t) => t ?? {})}
|
||||
{renderContainer?.((t) => ({
|
||||
id: 'nd-page',
|
||||
'data-full': full,
|
||||
children: (
|
||||
<>
|
||||
{renderBreadcrumb?.((t) => t ?? {})}
|
||||
{children}
|
||||
{renderFooter?.((t) => t ?? {})}
|
||||
</>
|
||||
),
|
||||
...t,
|
||||
className: cn(
|
||||
return wrapper(
|
||||
<>
|
||||
{tocPopoverEnabled &&
|
||||
(tocPopover ?? (
|
||||
<PageTOCPopover>
|
||||
<PageTOCPopoverContent>
|
||||
{tocOptions.header}
|
||||
<TOCScrollArea>
|
||||
{tocOptions.style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{tocOptions.footer}
|
||||
</PageTOCPopoverContent>
|
||||
<PageTOCPopoverTrigger />
|
||||
</PageTOCPopover>
|
||||
))}
|
||||
<article
|
||||
id="nd-page"
|
||||
data-full={full}
|
||||
className={cn(
|
||||
'flex flex-col w-full max-w-[900px] mx-auto [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14',
|
||||
full ? 'max-w-[1200px]' : 'xl:layout:[--fd-toc-width:268px]',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
</TOCProvider>
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{breadcrumbEnabled && (breadcrumb ?? <PageBreadcrumb {...breadcrumbProps} />)}
|
||||
{children}
|
||||
{footerEnabled !== false && (footerReplace ?? <PageFooter {...footerProps} />)}
|
||||
</article>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,21 +187,4 @@ export function DocsTitle({ children, className, ...props }: ComponentProps<'h1'
|
||||
);
|
||||
}
|
||||
|
||||
export function TOC({ container, trigger, content, header, footer, style }: TOCProps) {
|
||||
return (
|
||||
<PageTOCPopover {...container}>
|
||||
<PageTOCPopoverContent {...content}>
|
||||
{header}
|
||||
<TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{footer}
|
||||
</PageTOCPopoverContent>
|
||||
<PageTOCPopoverTrigger {...trigger} />
|
||||
</PageTOCPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageLastUpdate, PageBreadcrumb, PageFooter } from './client';
|
||||
export type { FooterProps, BreadcrumbProps } from './client';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/shared/page-actions';
|
||||
export { PageLastUpdate, PageBreadcrumb } from './client';
|
||||
|
||||
@@ -12,13 +12,7 @@ import {
|
||||
import { cva } from 'class-variance-authority';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import { cn } from '@/utils/cn';
|
||||
import {
|
||||
type LinkItemType,
|
||||
type NavOptions,
|
||||
parseLayoutProps,
|
||||
renderTitleNav,
|
||||
useLinkItems,
|
||||
} from '@/layouts/shared';
|
||||
import { type LinkItemType, type NavOptions, renderTitleNav, useLinkItems } from '@/layouts/shared';
|
||||
import { LinkItem } from '@/utils/link-item';
|
||||
import {
|
||||
NavigationMenuRoot,
|
||||
@@ -37,7 +31,6 @@ import { ChevronDown, Languages } from 'lucide-react';
|
||||
import { useIsScrollTop } from '@/utils/use-is-scroll-top';
|
||||
import { NavigationMenu } from '@base-ui/react';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { renderer } from '@/utils/renderer';
|
||||
|
||||
const MobileMenuContext = createContext<{
|
||||
open: boolean;
|
||||
@@ -63,21 +56,14 @@ export const navItemVariants = cva('[&_svg]:size-4', {
|
||||
},
|
||||
});
|
||||
|
||||
export function Header(props: HomeLayoutProps) {
|
||||
const {
|
||||
nav = {},
|
||||
LanguageSwitch,
|
||||
links,
|
||||
githubUrl,
|
||||
SearchToggle: SearchToggleRenderer,
|
||||
LargeSearchToggle: LargeSearchToggleRenderer,
|
||||
ThemeSwitch,
|
||||
} = parseLayoutProps(props);
|
||||
|
||||
const renderSearchToggle = renderer(SearchToggleRenderer, SearchToggle);
|
||||
const renderLargeSearchToggle = renderer(LargeSearchToggleRenderer, LargeSearchToggle);
|
||||
const renderThemeSwitch = renderer(ThemeSwitch, ThemeToggle);
|
||||
const renderLanguageSwitch = renderer(LanguageSwitch, LanguageToggle);
|
||||
export function Header({
|
||||
nav = {},
|
||||
i18n = false,
|
||||
links,
|
||||
githubUrl,
|
||||
themeSwitch = {},
|
||||
searchToggle = {},
|
||||
}: HomeLayoutProps) {
|
||||
const { menuItems, navItems } = useLinkItems({ links, githubUrl });
|
||||
|
||||
return (
|
||||
@@ -100,16 +86,20 @@ export function Header(props: HomeLayoutProps) {
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
<div className="flex flex-row items-center justify-end gap-1.5 flex-1 max-lg:hidden">
|
||||
{renderLargeSearchToggle?.((o) => ({
|
||||
hideIfDisabled: true,
|
||||
...o,
|
||||
className: cn('w-full rounded-full ps-2.5 max-w-[240px]', o?.className),
|
||||
}))}
|
||||
{renderThemeSwitch?.((t) => t ?? {})}
|
||||
{renderLanguageSwitch?.((o) => ({
|
||||
children: <Languages className="size-5" />,
|
||||
...o,
|
||||
}))}
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.lg ?? (
|
||||
<LargeSearchToggle
|
||||
className="w-full rounded-full ps-2.5 max-w-[240px]"
|
||||
hideIfDisabled
|
||||
/>
|
||||
))}
|
||||
{themeSwitch.enabled !== false &&
|
||||
(themeSwitch.component ?? <ThemeToggle mode={themeSwitch?.mode} />)}
|
||||
{i18n && (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-5" />
|
||||
</LanguageToggle>
|
||||
)}
|
||||
<NavigationMenuList className="flex flex-row gap-2 items-center empty:hidden">
|
||||
{navItems.filter(isSecondary).map((item, i) => (
|
||||
<NavigationMenuLinkItem
|
||||
@@ -121,11 +111,8 @@ export function Header(props: HomeLayoutProps) {
|
||||
</NavigationMenuList>
|
||||
</div>
|
||||
<div className="flex flex-row items-center ms-auto -me-1.5 lg:hidden">
|
||||
{renderSearchToggle?.((o) => ({
|
||||
hideIfDisabled: true,
|
||||
...o,
|
||||
className: cn('p-2', o?.className),
|
||||
}))}
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.sm ?? <SearchToggle className="p-2" hideIfDisabled />)}
|
||||
<CollapsibleTrigger
|
||||
aria-label="Toggle Menu"
|
||||
className={cn(
|
||||
@@ -155,17 +142,15 @@ export function Header(props: HomeLayoutProps) {
|
||||
/>
|
||||
))}
|
||||
<div role="separator" className="flex-1 sm:hidden" />
|
||||
{renderLanguageSwitch?.((o) => ({
|
||||
children: (
|
||||
<>
|
||||
<Languages className="size-5" />
|
||||
<LanguageToggleText />
|
||||
<ChevronDown className="size-3 text-fd-muted-foreground" />
|
||||
</>
|
||||
),
|
||||
...o,
|
||||
}))}
|
||||
{renderThemeSwitch?.((t) => t ?? {})}
|
||||
{i18n && (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-5" />
|
||||
<LanguageToggleText />
|
||||
<ChevronDown className="size-3 text-fd-muted-foreground" />
|
||||
</LanguageToggle>
|
||||
)}
|
||||
{themeSwitch.enabled !== false &&
|
||||
(themeSwitch.component ?? <ThemeToggle mode={themeSwitch?.mode} />)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
<NavigationMenu.Portal>
|
||||
|
||||
@@ -2,61 +2,32 @@ import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type BaseLayoutProps, type NavOptions } from '@/layouts/shared';
|
||||
import { Header } from '@/layouts/home/client';
|
||||
import { renderer, type Renderer } from '@/utils/renderer';
|
||||
|
||||
export interface HomeLayoutProps extends BaseLayoutProps {
|
||||
Container?: Renderer<ComponentProps<'main'>>;
|
||||
nav?: Partial<
|
||||
NavOptions & {
|
||||
/**
|
||||
* Open mobile menu when hovering the trigger
|
||||
*/
|
||||
enableHoverToOpen?: boolean;
|
||||
}
|
||||
>;
|
||||
nav?: Partial<NavOptions>;
|
||||
}
|
||||
|
||||
export function HomeLayout({
|
||||
nav = {},
|
||||
links,
|
||||
githubUrl,
|
||||
i18n,
|
||||
LanguageSwitch,
|
||||
ThemeSwitch,
|
||||
SearchToggle,
|
||||
LargeSearchToggle,
|
||||
themeSwitch = {},
|
||||
searchToggle,
|
||||
Container,
|
||||
children,
|
||||
...rest
|
||||
}: HomeLayoutProps & ComponentProps<'main'>) {
|
||||
const renderMain = renderer(Container ?? rest, 'main');
|
||||
export function HomeLayout(props: HomeLayoutProps & ComponentProps<'main'>) {
|
||||
const { nav = {}, links, githubUrl, i18n, themeSwitch = {}, searchToggle, ...rest } = props;
|
||||
|
||||
return renderMain?.((t) => ({
|
||||
id: 'nd-home-layout',
|
||||
...t,
|
||||
...rest,
|
||||
className: cn('flex flex-1 flex-col [--fd-layout-width:1400px]', t?.className, rest.className),
|
||||
children: (
|
||||
<>
|
||||
{nav.enabled !== false &&
|
||||
(nav.component ?? (
|
||||
<Header
|
||||
links={links}
|
||||
nav={nav}
|
||||
themeSwitch={themeSwitch}
|
||||
searchToggle={searchToggle}
|
||||
i18n={i18n}
|
||||
LanguageSwitch={LanguageSwitch}
|
||||
ThemeSwitch={ThemeSwitch}
|
||||
SearchToggle={SearchToggle}
|
||||
LargeSearchToggle={LargeSearchToggle}
|
||||
githubUrl={githubUrl}
|
||||
/>
|
||||
))}
|
||||
{children}
|
||||
</>
|
||||
),
|
||||
}));
|
||||
return (
|
||||
<main
|
||||
id="nd-home-layout"
|
||||
{...rest}
|
||||
className={cn('flex flex-1 flex-col [--fd-layout-width:1400px]', rest.className)}
|
||||
>
|
||||
{nav.enabled !== false &&
|
||||
(nav.component ?? (
|
||||
<Header
|
||||
links={links}
|
||||
nav={nav}
|
||||
themeSwitch={themeSwitch}
|
||||
searchToggle={searchToggle}
|
||||
i18n={i18n}
|
||||
githubUrl={githubUrl}
|
||||
/>
|
||||
))}
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,11 +16,10 @@ import { useSidebar } from '@/components/sidebar/base';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
import { isTabActive, type SidebarTabWithProps } from '@/components/sidebar/tabs/dropdown';
|
||||
import { useIsScrollTop } from '@/utils/use-is-scroll-top';
|
||||
import { LinkItem, type LinkItemType, type MenuItemType } from '@/utils/link-item';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { isTabActive, type SidebarTabWithProps } from '@/components/sidebar/tabs/dropdown';
|
||||
import { renderer, type Renderer } from '@/utils/renderer';
|
||||
|
||||
const LayoutContext = createContext<
|
||||
| (LayoutInfo & {
|
||||
@@ -82,45 +81,40 @@ export function LayoutHeader(props: ComponentProps<'header'>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutBody({
|
||||
_,
|
||||
children,
|
||||
}: {
|
||||
_: Renderer<ComponentProps<'div'>>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
export function LayoutBody({ className, style, children, ...props }: ComponentProps<'div'>) {
|
||||
const { navMode } = useNotebookLayout();
|
||||
const { collapsed } = useSidebar();
|
||||
const pageCol =
|
||||
'calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-col) - var(--fd-toc-width))';
|
||||
const render = renderer(_, 'div');
|
||||
|
||||
return render?.(
|
||||
(t) =>
|
||||
({
|
||||
id: 'nd-notebook-layout',
|
||||
children,
|
||||
...t,
|
||||
className: cn(
|
||||
'grid overflow-x-clip min-h-(--fd-docs-height) transition-[grid-template-columns] auto-cols-auto auto-rows-auto [--fd-docs-height:100dvh] [--fd-header-height:0px] [--fd-toc-popover-height:0px] [--fd-sidebar-width:0px] [--fd-toc-width:0px]',
|
||||
t?.className,
|
||||
),
|
||||
style: {
|
||||
return (
|
||||
<div
|
||||
id="nd-notebook-layout"
|
||||
className={cn(
|
||||
'grid overflow-x-clip min-h-(--fd-docs-height) transition-[grid-template-columns] auto-cols-auto auto-rows-auto [--fd-docs-height:100dvh] [--fd-header-height:0px] [--fd-toc-popover-height:0px] [--fd-sidebar-width:0px] [--fd-toc-width:0px]',
|
||||
className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
gridTemplate:
|
||||
navMode === 'top'
|
||||
? `". header header header ."
|
||||
"sidebar sidebar toc-popover toc-popover ."
|
||||
"sidebar sidebar main toc ." 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, ${pageCol}) var(--fd-toc-width) minmax(min-content, 1fr)`
|
||||
"sidebar sidebar toc-popover toc-popover ."
|
||||
"sidebar sidebar main toc ." 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, ${pageCol}) var(--fd-toc-width) minmax(min-content, 1fr)`
|
||||
: `"sidebar sidebar header header ."
|
||||
"sidebar sidebar toc-popover toc-popover ."
|
||||
"sidebar sidebar main toc ." 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, ${pageCol}) var(--fd-toc-width) minmax(min-content, 1fr)`,
|
||||
"sidebar sidebar toc-popover toc-popover ."
|
||||
"sidebar sidebar main toc ." 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, ${pageCol}) var(--fd-toc-width) minmax(min-content, 1fr)`,
|
||||
'--fd-docs-row-1': 'var(--fd-banner-height, 0px)',
|
||||
'--fd-docs-row-2': 'calc(var(--fd-docs-row-1) + var(--fd-header-height))',
|
||||
'--fd-docs-row-3': 'calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height))',
|
||||
'--fd-sidebar-col': collapsed ? '0px' : 'var(--fd-sidebar-width)',
|
||||
...t?.style,
|
||||
},
|
||||
}) as ComponentProps<'div'>,
|
||||
...style,
|
||||
} as object
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import {
|
||||
type ComponentProps,
|
||||
createElement,
|
||||
type FC,
|
||||
type HTMLAttributes,
|
||||
type ReactNode,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import {
|
||||
type BaseLayoutProps,
|
||||
parseLayoutProps,
|
||||
renderTitleNav,
|
||||
useLinkItems,
|
||||
} from '@/layouts/shared';
|
||||
import { type ComponentProps, type FC, type HTMLAttributes, type ReactNode, useMemo } from 'react';
|
||||
import { type BaseLayoutProps, renderTitleNav, useLinkItems } from '@/layouts/shared';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarCollapseTrigger,
|
||||
@@ -37,11 +25,10 @@ import {
|
||||
NavbarLinkItem,
|
||||
} from '@/layouts/notebook/client';
|
||||
import { LargeSearchToggle, SearchToggle } from '@/layouts/shared/search-toggle';
|
||||
import { LinkItem } from '@/utils/link-item';
|
||||
import { LinkItem, type LinkItemType } from '@/utils/link-item';
|
||||
import type { SidebarPageTreeComponents } from '@/components/sidebar/page-tree';
|
||||
import { getSidebarTabs, type GetSidebarTabsOptions } from '@/components/sidebar/tabs';
|
||||
import { SidebarTabsDropdown, type SidebarTabWithProps } from '@/components/sidebar/tabs/dropdown';
|
||||
import { type Renderer, renderer } from '@/utils/renderer';
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
@@ -53,12 +40,6 @@ export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
|
||||
sidebar?: SidebarOptions;
|
||||
|
||||
SidebarTrigger?: Renderer<ComponentProps<'button'>>;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
|
||||
/**
|
||||
* @deprecated use `Container` instead.
|
||||
*/
|
||||
containerProps?: HTMLAttributes<HTMLDivElement>;
|
||||
}
|
||||
|
||||
@@ -89,23 +70,13 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
tabMode = 'sidebar',
|
||||
nav = {},
|
||||
sidebar: { tabs: tabOptions, defaultOpenLevel, prefetch, ...sidebarProps } = {},
|
||||
LanguageSwitch,
|
||||
SearchToggle: searchToggleRenderer,
|
||||
LargeSearchToggle: largeSearchToggleRenderer,
|
||||
ThemeSwitch,
|
||||
SidebarTrigger: SidebarTriggerRenderer = true,
|
||||
containerProps,
|
||||
Container = containerProps ?? true,
|
||||
i18n = false,
|
||||
themeSwitch = {},
|
||||
tree,
|
||||
} = parseLayoutProps(props);
|
||||
} = props;
|
||||
|
||||
const navMode = nav.mode ?? 'auto';
|
||||
const { menuItems, navItems } = useLinkItems(props);
|
||||
const renderSearchToggle = renderer(searchToggleRenderer, SearchToggle);
|
||||
const renderLargeSearchToggle = renderer(largeSearchToggleRenderer, LargeSearchToggle);
|
||||
const renderThemeSwitch = renderer(ThemeSwitch, ThemeToggle);
|
||||
const renderLanguageSwitch = renderer(LanguageSwitch, LanguageToggle);
|
||||
const renderSidebarTrigger = renderer(SidebarTriggerRenderer, SidebarTrigger);
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
if (Array.isArray(tabOptions)) {
|
||||
return tabOptions;
|
||||
@@ -124,40 +95,33 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
|
||||
function sidebar() {
|
||||
const { banner, footer, components, collapsible = true, ...rest } = sidebarProps;
|
||||
|
||||
const iconLinks = menuItems.filter((item) => item.type === 'icon');
|
||||
|
||||
function renderHeader(props: ComponentProps<'div'>) {
|
||||
if (typeof banner === 'function') return createElement(banner, props);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('flex flex-col gap-3 p-4 pb-2 empty:hidden', props.className)}
|
||||
>
|
||||
{props.children}
|
||||
{banner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFooter(props: ComponentProps<'div'>) {
|
||||
if (typeof footer === 'function') return createElement(footer, props);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'hidden flex-row text-fd-muted-foreground items-center border-t p-4 pt-2',
|
||||
iconLinks.length > 0 && 'max-lg:flex',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Header =
|
||||
typeof banner === 'function'
|
||||
? banner
|
||||
: ({ className, ...props }: ComponentProps<'div'>) => (
|
||||
<div className={cn('flex flex-col gap-3 p-4 pb-2 empty:hidden', className)} {...props}>
|
||||
{props.children}
|
||||
{banner}
|
||||
</div>
|
||||
);
|
||||
const Footer =
|
||||
typeof footer === 'function'
|
||||
? footer
|
||||
: ({ className, ...props }: ComponentProps<'div'>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'hidden flex-row text-fd-muted-foreground items-center border-t p-4 pt-2',
|
||||
iconLinks.length > 0 && 'max-lg:flex',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
const viewport = (
|
||||
<SidebarViewport>
|
||||
{menuItems
|
||||
@@ -177,41 +141,37 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SidebarContent {...rest}>
|
||||
{renderHeader({
|
||||
children: (
|
||||
<>
|
||||
{navMode === 'auto' && (
|
||||
<div className="flex justify-between">
|
||||
{renderTitleNav(nav, {
|
||||
className: 'inline-flex items-center gap-2.5 font-medium',
|
||||
})}
|
||||
{collapsible && (
|
||||
<SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'mt-px mb-auto text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
<Header>
|
||||
{navMode === 'auto' && (
|
||||
<div className="flex justify-between">
|
||||
{renderTitleNav(nav, {
|
||||
className: 'inline-flex items-center gap-2.5 font-medium',
|
||||
})}
|
||||
{collapsible && (
|
||||
<SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'mt-px mb-auto text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
)}
|
||||
{tabs.length > 0 && (
|
||||
<SidebarTabsDropdown
|
||||
options={tabs}
|
||||
className={cn(tabMode === 'navbar' && 'lg:hidden')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{tabs.length > 0 && (
|
||||
<SidebarTabsDropdown
|
||||
options={tabs}
|
||||
className={cn(tabMode === 'navbar' && 'lg:hidden')}
|
||||
/>
|
||||
)}
|
||||
</Header>
|
||||
{viewport}
|
||||
{renderFooter({
|
||||
children: iconLinks.map((item, i) => (
|
||||
<Footer>
|
||||
{iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
@@ -226,205 +186,75 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
)),
|
||||
})}
|
||||
))}
|
||||
</Footer>
|
||||
</SidebarContent>
|
||||
<SidebarDrawer {...rest}>
|
||||
{renderHeader({
|
||||
children: (
|
||||
<>
|
||||
<SidebarTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon-sm',
|
||||
color: 'ghost',
|
||||
className: 'ms-auto text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<X />
|
||||
</SidebarTrigger>
|
||||
{tabs.length > 0 && <SidebarTabsDropdown options={tabs} />}
|
||||
</>
|
||||
),
|
||||
})}
|
||||
<Header>
|
||||
<SidebarTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon-sm',
|
||||
color: 'ghost',
|
||||
className: 'ms-auto text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<X />
|
||||
</SidebarTrigger>
|
||||
{tabs.length > 0 && <SidebarTabsDropdown options={tabs} />}
|
||||
</Header>
|
||||
{viewport}
|
||||
{renderFooter({
|
||||
className: cn(
|
||||
'hidden flex-row items-center justify-end',
|
||||
(renderLanguageSwitch || renderThemeSwitch) && 'flex',
|
||||
iconLinks.length > 0 && 'max-lg:flex',
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon-sm',
|
||||
color: 'ghost',
|
||||
}),
|
||||
'text-fd-muted-foreground lg:hidden',
|
||||
i === iconLinks.length - 1 && 'me-auto',
|
||||
)}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
))}
|
||||
{renderLanguageSwitch?.((o) => ({
|
||||
children: <Languages className="size-4.5 text-fd-muted-foreground" />,
|
||||
...o,
|
||||
}))}
|
||||
{renderThemeSwitch?.((t) => t ?? {})}
|
||||
</>
|
||||
),
|
||||
})}
|
||||
</SidebarDrawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function header() {
|
||||
const sidebarCollapsible = sidebarProps.collapsible ?? true;
|
||||
const showLayoutTabs = tabMode === 'navbar' && tabs.length > 0;
|
||||
|
||||
return (
|
||||
<LayoutHeader
|
||||
id="nd-subnav"
|
||||
className={cn(
|
||||
'sticky [grid-area:header] flex flex-col top-(--fd-docs-row-1) z-10 backdrop-blur-sm transition-colors data-[transparent=false]:bg-fd-background/80 layout:[--fd-header-height:--spacing(14)]',
|
||||
showLayoutTabs && 'lg:layout:[--fd-header-height:--spacing(24)]',
|
||||
)}
|
||||
>
|
||||
<div data-header-body="" className="flex border-b px-4 gap-2 h-14 md:px-6">
|
||||
<div
|
||||
<Footer
|
||||
className={cn(
|
||||
'items-center',
|
||||
navMode === 'top' && 'flex flex-1',
|
||||
navMode === 'auto' && 'hidden has-data-[collapsed=true]:md:flex max-md:flex',
|
||||
'hidden flex-row items-center justify-end',
|
||||
(i18n || themeSwitch.enabled !== false) && 'flex',
|
||||
iconLinks.length > 0 && 'max-lg:flex',
|
||||
)}
|
||||
>
|
||||
{sidebarCollapsible && navMode === 'auto' && (
|
||||
<SidebarCollapseTrigger
|
||||
{iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
color: 'ghost',
|
||||
}),
|
||||
'text-fd-muted-foreground data-[collapsed=false]:hidden max-md:hidden',
|
||||
'text-fd-muted-foreground lg:hidden',
|
||||
i === iconLinks.length - 1 && 'me-auto',
|
||||
)}
|
||||
aria-label={item.label}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
))}
|
||||
{i18n && (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-4.5 text-fd-muted-foreground" />
|
||||
</LanguageToggle>
|
||||
)}
|
||||
{renderTitleNav(nav, {
|
||||
className: cn(
|
||||
'inline-flex items-center gap-2.5 font-semibold',
|
||||
navMode === 'auto' && 'md:hidden',
|
||||
),
|
||||
})}
|
||||
{nav.children}
|
||||
</div>
|
||||
{renderLargeSearchToggle?.((o) => ({
|
||||
hideIfDisabled: true,
|
||||
...o,
|
||||
className: cn(
|
||||
'w-full my-auto max-md:hidden',
|
||||
navMode === 'top' ? 'ps-2.5 rounded-xl max-w-sm' : 'max-w-[240px]',
|
||||
o?.className,
|
||||
),
|
||||
}))}
|
||||
<div className="flex flex-1 items-center justify-end md:gap-2">
|
||||
<div className="flex items-center gap-6 empty:hidden max-lg:hidden">
|
||||
{navItems
|
||||
.filter((item) => item.type !== 'icon')
|
||||
.map((item, i) => (
|
||||
<NavbarLinkItem key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
{navItems
|
||||
.filter((item) => item.type === 'icon')
|
||||
.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({ size: 'icon-sm', color: 'ghost' }),
|
||||
'text-fd-muted-foreground max-lg:hidden',
|
||||
)}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
{themeSwitch.enabled !== false &&
|
||||
(themeSwitch.component ?? (
|
||||
<ThemeToggle mode={themeSwitch.mode ?? 'light-dark-system'} />
|
||||
))}
|
||||
|
||||
<div className="flex items-center md:hidden">
|
||||
{renderSearchToggle?.((o) => ({
|
||||
hideIfDisabled: true,
|
||||
...o,
|
||||
className: cn('p-2', o?.className),
|
||||
}))}
|
||||
{renderSidebarTrigger?.((t) => ({
|
||||
children: <SidebarIcon />,
|
||||
...t,
|
||||
className: cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
}),
|
||||
'p-2 -me-1.5',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 max-md:hidden">
|
||||
{renderLanguageSwitch?.((o) => ({
|
||||
children: <Languages className="size-4.5 text-fd-muted-foreground" />,
|
||||
...o,
|
||||
}))}
|
||||
{renderThemeSwitch?.((t) => t ?? {})}
|
||||
{sidebarCollapsible && navMode === 'top' && (
|
||||
<SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'icon-sm',
|
||||
}),
|
||||
'text-fd-muted-foreground rounded-full -me-1.5',
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showLayoutTabs && (
|
||||
<LayoutHeaderTabs
|
||||
data-header-tabs=""
|
||||
className="overflow-x-auto border-b px-6 h-10 max-lg:hidden"
|
||||
options={tabs}
|
||||
/>
|
||||
)}
|
||||
</LayoutHeader>
|
||||
</Footer>
|
||||
</SidebarDrawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeContextProvider tree={tree}>
|
||||
<LayoutContextProvider
|
||||
navMode={navMode}
|
||||
navMode={nav.mode ?? 'auto'}
|
||||
tabMode={tabMode}
|
||||
navTransparentMode={nav.transparentMode}
|
||||
>
|
||||
<Sidebar defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
|
||||
<LayoutBody _={Container}>
|
||||
<LayoutBody {...props.containerProps}>
|
||||
{sidebar()}
|
||||
{header()}
|
||||
<DocsNavbar {...props} links={navItems} tabs={tabs} />
|
||||
{props.children}
|
||||
</LayoutBody>
|
||||
</Sidebar>
|
||||
@@ -432,3 +262,152 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
</TreeContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function DocsNavbar({
|
||||
links,
|
||||
tabs,
|
||||
tabMode = 'sidebar',
|
||||
sidebar: { collapsible: sidebarCollapsible = true } = {},
|
||||
searchToggle = {},
|
||||
themeSwitch = {},
|
||||
nav = {},
|
||||
i18n,
|
||||
}: DocsLayoutProps & {
|
||||
links: LinkItemType[];
|
||||
tabs: SidebarTabWithProps[];
|
||||
}) {
|
||||
const navMode = nav.mode ?? 'auto';
|
||||
const showLayoutTabs = tabMode === 'navbar' && tabs.length > 0;
|
||||
|
||||
return (
|
||||
<LayoutHeader
|
||||
id="nd-subnav"
|
||||
className={cn(
|
||||
'sticky [grid-area:header] flex flex-col top-(--fd-docs-row-1) z-10 backdrop-blur-sm transition-colors data-[transparent=false]:bg-fd-background/80 layout:[--fd-header-height:--spacing(14)]',
|
||||
showLayoutTabs && 'lg:layout:[--fd-header-height:--spacing(24)]',
|
||||
)}
|
||||
>
|
||||
<div data-header-body="" className="flex border-b px-4 gap-2 h-14 md:px-6">
|
||||
<div
|
||||
className={cn(
|
||||
'items-center',
|
||||
navMode === 'top' && 'flex flex-1',
|
||||
navMode === 'auto' && 'hidden has-data-[collapsed=true]:md:flex max-md:flex',
|
||||
)}
|
||||
>
|
||||
{sidebarCollapsible && navMode === 'auto' && (
|
||||
<SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
}),
|
||||
'text-fd-muted-foreground data-[collapsed=false]:hidden max-md:hidden',
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
)}
|
||||
{renderTitleNav(nav, {
|
||||
className: cn(
|
||||
'inline-flex items-center gap-2.5 font-semibold',
|
||||
navMode === 'auto' && 'md:hidden',
|
||||
),
|
||||
})}
|
||||
{nav.children}
|
||||
</div>
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.lg ? (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full my-auto max-md:hidden',
|
||||
navMode === 'top' ? 'rounded-xl max-w-sm' : 'max-w-[240px]',
|
||||
)}
|
||||
>
|
||||
{searchToggle.components.lg}
|
||||
</div>
|
||||
) : (
|
||||
<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 items-center justify-end md:gap-2">
|
||||
<div className="flex items-center gap-6 empty:hidden max-lg:hidden">
|
||||
{links
|
||||
.filter((item) => item.type !== 'icon')
|
||||
.map((item, i) => (
|
||||
<NavbarLinkItem key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
{links
|
||||
.filter((item) => item.type === 'icon')
|
||||
.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({ size: 'icon-sm', color: 'ghost' }),
|
||||
'text-fd-muted-foreground max-lg:hidden',
|
||||
)}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
))}
|
||||
|
||||
<div className="flex items-center md:hidden">
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.sm ?? <SearchToggle hideIfDisabled className="p-2" />)}
|
||||
<SidebarTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'p-2 -me-1.5',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarTrigger>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 max-md:hidden">
|
||||
{i18n && (
|
||||
<LanguageToggle>
|
||||
<Languages className="size-4.5 text-fd-muted-foreground" />
|
||||
</LanguageToggle>
|
||||
)}
|
||||
{themeSwitch.enabled !== false &&
|
||||
(themeSwitch.component ?? (
|
||||
<ThemeToggle mode={themeSwitch.mode ?? 'light-dark-system'} />
|
||||
))}
|
||||
{sidebarCollapsible && navMode === 'top' && (
|
||||
<SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'icon-sm',
|
||||
}),
|
||||
'text-fd-muted-foreground rounded-full -me-1.5',
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showLayoutTabs && (
|
||||
<LayoutHeaderTabs
|
||||
data-header-tabs=""
|
||||
className="overflow-x-auto border-b px-6 h-10 max-lg:hidden"
|
||||
options={tabs}
|
||||
/>
|
||||
)}
|
||||
</LayoutHeader>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,21 +16,21 @@ import type { AnchorProviderProps, TOCItemType } from 'fumadocs-core/toc';
|
||||
import * as TocDefault from '@/components/toc/default';
|
||||
import * as TocClerk from '@/components/toc/clerk';
|
||||
import { TOCProvider, TOCScrollArea } from '@/components/toc';
|
||||
import { ChildrenRenderer, renderer, type Renderer } from '@/utils/renderer';
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
export interface DocsPageProps {
|
||||
toc?: TOCItemType[];
|
||||
/**
|
||||
* - `multiple` (default): Accept multiple active items
|
||||
* - `single`: Only accept one active item at most
|
||||
* */
|
||||
tocMode?: 'single' | 'multiple';
|
||||
TOC?: Renderer<TOCMainProps>;
|
||||
TOCPopover?: Renderer<TOCPopoverProps>;
|
||||
/** Footer navigation, located under the page body. */
|
||||
Footer?: Renderer<FooterProps>;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
Breadcrumb?: Renderer<BreadcrumbProps>;
|
||||
tableOfContent?: Partial<TableOfContentOptions>;
|
||||
tableOfContentPopover?: Partial<TableOfContentPopoverOptions>;
|
||||
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
@@ -38,51 +38,28 @@ export interface DocsPageProps {
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
|
||||
/**
|
||||
* Replace or disable breadcrumb
|
||||
*/
|
||||
breadcrumb?: Partial<BreadcrumbOptions>;
|
||||
|
||||
/**
|
||||
* Footer navigation, located under the page body.
|
||||
*
|
||||
* You can specify `footer.children` to add extra components under the footer.
|
||||
*/
|
||||
footer?: Partial<FooterOptions>;
|
||||
|
||||
children?: ReactNode;
|
||||
|
||||
/** @deprecated use `Footer` instead. */
|
||||
footer?: FooterOptions;
|
||||
/** @deprecated use `Container` instead. */
|
||||
/**
|
||||
* Apply class names to the `#nd-page` container.
|
||||
*/
|
||||
className?: string;
|
||||
/** @deprecated use `Breadcrumb` instead. */
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
/** @deprecated use `TOC` instead, or `tocMode` for enabling `single`. */
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
/** @deprecated use `TOCPopover` instead. */
|
||||
tableOfContentPopover?: TableOfContentPopoverOptions;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface TableOfContentOptions extends Pick<AnchorProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface TableOfContentPopoverOptions extends TOCProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
export interface TOCPopoverProps extends TOCProps {
|
||||
container?: ComponentProps<typeof PageTOCPopover>;
|
||||
trigger?: ComponentProps<typeof PageTOCPopoverTrigger>;
|
||||
content?: ComponentProps<typeof PageTOCPopoverContent>;
|
||||
}
|
||||
|
||||
export interface TOCMainProps extends TOCProps {
|
||||
container?: ComponentProps<typeof PageTOCPopover>;
|
||||
}
|
||||
|
||||
interface TOCProps {
|
||||
type TableOfContentOptions = Pick<AnchorProviderProps, 'single'> & {
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
@@ -93,82 +70,103 @@ interface TOCProps {
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
|
||||
enabled: boolean;
|
||||
component: ReactNode;
|
||||
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
style?: 'normal' | 'clerk';
|
||||
}
|
||||
};
|
||||
|
||||
type TableOfContentPopoverOptions = Omit<TableOfContentOptions, 'single'>;
|
||||
|
||||
export function DocsPage({
|
||||
tableOfContent: tocProps = {},
|
||||
TOC: TOCRenderer,
|
||||
tableOfContentPopover: tocPopoverProps = {},
|
||||
TOCPopover: TOCPopoverRenderer,
|
||||
footer = {},
|
||||
Footer = footer.enabled === false
|
||||
? false
|
||||
: footer.component
|
||||
? new ChildrenRenderer(footer.component)
|
||||
: footer,
|
||||
className,
|
||||
Container = { className },
|
||||
breadcrumb = {},
|
||||
Breadcrumb = breadcrumb.enabled === false
|
||||
? false
|
||||
: breadcrumb.component
|
||||
? new ChildrenRenderer(breadcrumb.component)
|
||||
: breadcrumb,
|
||||
breadcrumb: { enabled: breadcrumbEnabled = true, component: breadcrumb, ...breadcrumbProps } = {},
|
||||
footer: { enabled: footerEnabled, component: footerReplace, ...footerProps } = {},
|
||||
full = false,
|
||||
tableOfContentPopover: {
|
||||
enabled: tocPopoverEnabled,
|
||||
component: tocPopover,
|
||||
...tocPopoverOptions
|
||||
} = {},
|
||||
tableOfContent: { enabled: tocEnabled, component: tocReplace, ...tocOptions } = {},
|
||||
toc = [],
|
||||
tocMode = tocProps.single ? 'single' : 'multiple',
|
||||
children,
|
||||
className,
|
||||
}: DocsPageProps) {
|
||||
if (!full && (tocProps.enabled ?? (toc.length > 0 || tocProps.footer || tocProps.header))) {
|
||||
TOCRenderer ??= tocProps.component ? new ChildrenRenderer(tocProps.component) : tocProps;
|
||||
} else {
|
||||
// force disable toc in full mode or when no content
|
||||
TOCRenderer = false;
|
||||
// disable TOC on full mode, you can still enable it with `enabled` option.
|
||||
tocEnabled ??=
|
||||
!full && (toc.length > 0 || tocOptions.footer !== undefined || tocOptions.header !== undefined);
|
||||
|
||||
tocPopoverEnabled ??=
|
||||
toc.length > 0 ||
|
||||
tocPopoverOptions.header !== undefined ||
|
||||
tocPopoverOptions.footer !== undefined;
|
||||
|
||||
let wrapper = (children: ReactNode) => children;
|
||||
|
||||
if (tocEnabled || tocPopoverEnabled) {
|
||||
wrapper = (children) => (
|
||||
<TOCProvider single={tocOptions.single} toc={toc}>
|
||||
{children}
|
||||
</TOCProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
tocPopoverProps.enabled ??
|
||||
(toc.length > 0 || tocPopoverProps.header || tocPopoverProps.footer)
|
||||
) {
|
||||
TOCPopoverRenderer ??= tocPopoverProps.component
|
||||
? new ChildrenRenderer(tocPopoverProps.component)
|
||||
: tocPopoverProps;
|
||||
} else {
|
||||
TOCPopoverRenderer = false;
|
||||
}
|
||||
|
||||
const renderBreadcrumb = renderer(Breadcrumb, PageBreadcrumb);
|
||||
const renderFooter = renderer(Footer, PageFooter);
|
||||
const renderContainer = renderer(Container, 'article');
|
||||
const renderToc = renderer(TOCRenderer, TOC);
|
||||
const renderTocPopover = renderer(TOCPopoverRenderer, TOCPopover);
|
||||
|
||||
return (
|
||||
<TOCProvider single={tocMode === 'single'} toc={renderToc || renderTocPopover ? toc : []}>
|
||||
{renderTocPopover?.((t) => t ?? {})}
|
||||
{renderContainer?.((t) => ({
|
||||
id: 'nd-page',
|
||||
'data-full': full,
|
||||
children: (
|
||||
<>
|
||||
{renderBreadcrumb?.((t) => t ?? {})}
|
||||
{children}
|
||||
{renderFooter?.((t) => t ?? {})}
|
||||
</>
|
||||
),
|
||||
...t,
|
||||
className: cn(
|
||||
return wrapper(
|
||||
<>
|
||||
{tocPopoverEnabled &&
|
||||
(tocPopover ?? (
|
||||
<PageTOCPopover>
|
||||
<PageTOCPopoverTrigger />
|
||||
<PageTOCPopoverContent>
|
||||
{tocPopoverOptions.header}
|
||||
<TOCScrollArea>
|
||||
{tocPopoverOptions.style === 'clerk' ? (
|
||||
<TocClerk.TOCItems />
|
||||
) : (
|
||||
<TocDefault.TOCItems />
|
||||
)}
|
||||
</TOCScrollArea>
|
||||
{tocPopoverOptions.footer}
|
||||
</PageTOCPopoverContent>
|
||||
</PageTOCPopover>
|
||||
))}
|
||||
<article
|
||||
id="nd-page"
|
||||
data-full={full}
|
||||
className={cn(
|
||||
'flex flex-col [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14 *:max-w-[900px]',
|
||||
full && '*:max-w-[1285px]',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
{renderToc?.((t) => t ?? {})}
|
||||
</TOCProvider>
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{breadcrumbEnabled && (breadcrumb ?? <PageBreadcrumb {...breadcrumbProps} />)}
|
||||
{children}
|
||||
{footerEnabled !== false && (footerReplace ?? <PageFooter {...footerProps} />)}
|
||||
</article>
|
||||
{tocEnabled &&
|
||||
(tocReplace ?? (
|
||||
<div
|
||||
id="nd-toc"
|
||||
className="sticky top-(--fd-docs-row-3) [grid-area:toc] h-[calc(var(--fd-docs-height)-var(--fd-docs-row-3))] flex flex-col w-(--fd-toc-width) pt-12 pe-4 pb-2 xl:layout:[--fd-toc-width:268px] max-xl:hidden"
|
||||
>
|
||||
{tocOptions.header}
|
||||
<h3
|
||||
id="toc-title"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground"
|
||||
>
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
{tocOptions.style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{tocOptions.footer}
|
||||
</div>
|
||||
))}
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,54 +225,4 @@ export function DocsTitle({ children, className, ...props }: ComponentProps<'h1'
|
||||
);
|
||||
}
|
||||
|
||||
export function TOC({ container, header, footer, style }: TOCMainProps) {
|
||||
return (
|
||||
<div
|
||||
id="nd-toc"
|
||||
{...container}
|
||||
className={cn(
|
||||
'sticky top-(--fd-docs-row-3) [grid-area:toc] h-[calc(var(--fd-docs-height)-var(--fd-docs-row-3))] flex flex-col w-(--fd-toc-width) pt-12 pe-4 pb-2 xl:layout:[--fd-toc-width:268px] max-xl:hidden',
|
||||
container?.className,
|
||||
)}
|
||||
>
|
||||
{header}
|
||||
<h3
|
||||
id="toc-title"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground"
|
||||
>
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TOCPopover({
|
||||
container,
|
||||
trigger,
|
||||
content,
|
||||
header,
|
||||
footer,
|
||||
style,
|
||||
}: TOCPopoverProps) {
|
||||
return (
|
||||
<PageTOCPopover {...container}>
|
||||
<PageTOCPopoverTrigger {...trigger} />
|
||||
<PageTOCPopoverContent {...content}>
|
||||
{header}
|
||||
<TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{footer}
|
||||
</PageTOCPopoverContent>
|
||||
</PageTOCPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageLastUpdate, PageBreadcrumb, PageFooter } from './client';
|
||||
export type { FooterProps, BreadcrumbProps } from './client';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/shared/page-actions';
|
||||
export { PageLastUpdate, PageBreadcrumb } from './client';
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { type HTMLAttributes, useMemo, type ComponentProps, type ReactNode } from 'react';
|
||||
import { useMemo, type ComponentProps, type ReactNode } from 'react';
|
||||
import type { I18nConfig } from 'fumadocs-core/i18n';
|
||||
import type { LinkItemType } from '@/utils/link-item';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import type { LargeSearchToggleProps, SearchToggleProps } from './search-toggle';
|
||||
import type { ThemeToggleProps } from './theme-toggle';
|
||||
import { ChildrenRenderer, type Renderer } from '@/utils/renderer';
|
||||
|
||||
export interface NavOptions {
|
||||
enabled: boolean;
|
||||
@@ -29,10 +26,27 @@ export interface NavOptions {
|
||||
}
|
||||
|
||||
export interface BaseLayoutProps {
|
||||
ThemeSwitch?: Renderer<ThemeToggleProps>;
|
||||
SearchToggle?: Renderer<SearchToggleProps>;
|
||||
LargeSearchToggle?: Renderer<LargeSearchToggleProps>;
|
||||
LanguageSwitch?: Renderer<HTMLAttributes<HTMLElement>>;
|
||||
themeSwitch?: {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
mode?: 'light-dark' | 'light-dark-system';
|
||||
};
|
||||
|
||||
searchToggle?: Partial<{
|
||||
enabled: boolean;
|
||||
components: Partial<{
|
||||
sm: ReactNode;
|
||||
lg: ReactNode;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* I18n options
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
i18n?: boolean | I18nConfig;
|
||||
|
||||
/**
|
||||
* GitHub url
|
||||
*/
|
||||
@@ -45,55 +59,6 @@ export interface BaseLayoutProps {
|
||||
nav?: Partial<NavOptions>;
|
||||
|
||||
children?: ReactNode;
|
||||
|
||||
/**
|
||||
* @deprecated use `LanguageSwitch` instead, this is now optional for i18n setups.
|
||||
* @defaultValue false
|
||||
*/
|
||||
i18n?: boolean | I18nConfig;
|
||||
/** @deprecated use `ThemeSwitch` instead */
|
||||
themeSwitch?: {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
mode?: 'light-dark' | 'light-dark-system';
|
||||
};
|
||||
/** @deprecated use `SearchToggle` & `LargeSearchToggle` instead */
|
||||
searchToggle?: {
|
||||
enabled?: boolean;
|
||||
components?: {
|
||||
sm?: ReactNode;
|
||||
lg?: ReactNode;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLayoutProps<T extends BaseLayoutProps>({
|
||||
i18n = false,
|
||||
themeSwitch = {},
|
||||
searchToggle = {},
|
||||
...rest
|
||||
}: T) {
|
||||
rest.LanguageSwitch ??= !!i18n;
|
||||
|
||||
if (themeSwitch.enabled === false) {
|
||||
rest.ThemeSwitch ??= false;
|
||||
} else if (themeSwitch.component) {
|
||||
rest.ThemeSwitch ??= new ChildrenRenderer(themeSwitch.component);
|
||||
} else {
|
||||
rest.ThemeSwitch ??= { mode: themeSwitch.mode };
|
||||
}
|
||||
|
||||
if (searchToggle.enabled === false) {
|
||||
rest.SearchToggle ??= false;
|
||||
rest.LargeSearchToggle ??= false;
|
||||
} else {
|
||||
const { sm, lg } = searchToggle.components ?? {};
|
||||
rest.SearchToggle ??= sm ? new ChildrenRenderer(sm) : true;
|
||||
rest.LargeSearchToggle ??= lg ? new ChildrenRenderer(lg) : true;
|
||||
}
|
||||
|
||||
return rest as Omit<T, 'themeSwitch' | 'i18n' | 'searchToggle'> &
|
||||
Pick<Required<T>, 'LanguageSwitch' | 'SearchToggle' | 'LargeSearchToggle' | 'ThemeSwitch'>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +106,7 @@ export function useLinkItems({ githubUrl, links }: Pick<BaseLayoutProps, 'links'
|
||||
const menuItems: LinkItemType[] = [];
|
||||
|
||||
for (const item of all) {
|
||||
switch (item.on) {
|
||||
switch (item.on ?? 'all') {
|
||||
case 'menu':
|
||||
menuItems.push(item);
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { cn } from '@/utils/cn';
|
||||
import { useCopyButton } from '@/utils/use-copy-button';
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
|
||||
const cache = new Map<string, Promise<string>>();
|
||||
|
||||
@@ -79,11 +78,8 @@ export function ViewOptionsPopover({
|
||||
*/
|
||||
githubUrl: string;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const items = useMemo(() => {
|
||||
const pageUrl =
|
||||
typeof window === 'undefined' ? pathname : new URL(pathname, window.location.origin);
|
||||
const q = `Read ${pageUrl}, I want to ask questions about it.`;
|
||||
const q = `Read ${markdownUrl}, I want to ask questions about it.`;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -218,7 +214,7 @@ export function ViewOptionsPopover({
|
||||
})}`,
|
||||
},
|
||||
];
|
||||
}, [githubUrl, markdownUrl, pathname]);
|
||||
}, [githubUrl, markdownUrl]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useI18n } from '@/contexts/i18n';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type ButtonProps, buttonVariants } from '@/components/ui/button';
|
||||
|
||||
export interface SearchToggleProps extends Omit<ComponentProps<'button'>, 'color'>, ButtonProps {
|
||||
interface SearchToggleProps extends Omit<ComponentProps<'button'>, 'color'>, ButtonProps {
|
||||
hideIfDisabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -40,11 +40,12 @@ export function SearchToggle({
|
||||
);
|
||||
}
|
||||
|
||||
export interface LargeSearchToggleProps extends ComponentProps<'button'> {
|
||||
export function LargeSearchToggle({
|
||||
hideIfDisabled,
|
||||
...props
|
||||
}: ComponentProps<'button'> & {
|
||||
hideIfDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function LargeSearchToggle({ hideIfDisabled, ...props }: LargeSearchToggleProps) {
|
||||
}) {
|
||||
const { enabled, hotKey, setOpenSearch } = useSearchContext();
|
||||
const { text } = useI18n();
|
||||
if (hideIfDisabled && !enabled) return null;
|
||||
@@ -55,7 +56,7 @@ export function LargeSearchToggle({ hideIfDisabled, ...props }: LargeSearchToggl
|
||||
data-search-full=""
|
||||
{...props}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 rounded-lg border bg-fd-secondary/50 p-1.5 ps-2 text-sm text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||
'inline-flex items-center gap-2 rounded-lg border bg-fd-secondary/50 p-1.5 ps-2 text-sm text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring',
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
|
||||
@@ -16,11 +16,13 @@ const itemVariants = cva('size-6.5 p-1.5 text-fd-muted-foreground', {
|
||||
|
||||
const full = [['light', Sun] as const, ['dark', Moon] as const, ['system', Airplay] as const];
|
||||
|
||||
export interface ThemeToggleProps extends ComponentProps<'div'> {
|
||||
export function ThemeToggle({
|
||||
className,
|
||||
mode = 'light-dark',
|
||||
...props
|
||||
}: ComponentProps<'div'> & {
|
||||
mode?: 'light-dark' | 'light-dark-system';
|
||||
}
|
||||
|
||||
export function ThemeToggle({ className, mode = 'light-dark', ...props }: ThemeToggleProps) {
|
||||
}) {
|
||||
const { setTheme, theme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { createElement, type ElementType, type FC, type ReactNode } from 'react';
|
||||
|
||||
export type Renderer<Props> = FC<Props> | Partial<Props> | ChildrenRenderer | boolean;
|
||||
|
||||
export type RenderFn<Props> = (props: (override?: Partial<Props>) => Props) => ReactNode;
|
||||
|
||||
export function renderer<Props extends object>(
|
||||
T: Renderer<Props>,
|
||||
Default: FC<Props> | ElementType<Props>,
|
||||
): RenderFn<Props> | undefined {
|
||||
if (T === false) return;
|
||||
if (T === true)
|
||||
return function Wrapper(props) {
|
||||
return createElement(Default, props());
|
||||
};
|
||||
if (T instanceof ChildrenRenderer)
|
||||
return function Wrapper() {
|
||||
return T.children;
|
||||
};
|
||||
if (typeof T === 'object')
|
||||
return function Wrapper(props) {
|
||||
return createElement(Default, props(T));
|
||||
};
|
||||
return function Wrapper(props) {
|
||||
return createElement(T, props());
|
||||
};
|
||||
}
|
||||
|
||||
export class ChildrenRenderer {
|
||||
constructor(readonly children: ReactNode) {}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ export default defineConfig({
|
||||
'./src/*.{ts,tsx}',
|
||||
'./src/{components,tailwind,contexts,layouts,provider,og}/**/*.{ts,tsx}',
|
||||
'./src/utils/use-*.{ts,tsx}',
|
||||
'./src/utils/renderer.ts',
|
||||
],
|
||||
fixedExtension: false,
|
||||
unbundle: true,
|
||||
|
||||
@@ -16,94 +16,110 @@
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./style.css": "./dist/style.css",
|
||||
"./css/*": "./css/*",
|
||||
"./image-zoom.css": "./dist/image-zoom.css",
|
||||
"./components/tabs.unstyled": {
|
||||
"types": "./dist/components/ui/tabs.d.ts",
|
||||
"import": "./dist/components/ui/tabs.js"
|
||||
},
|
||||
"./components/*": {
|
||||
"types": "./dist/components/*.d.ts",
|
||||
"import": "./dist/components/*.js",
|
||||
"default": "./dist/components/*.js"
|
||||
},
|
||||
"./i18n": {
|
||||
"types": "./dist/i18n.d.ts",
|
||||
"import": "./dist/i18n.js"
|
||||
},
|
||||
"./layouts/home": {
|
||||
"types": "./dist/layouts/home/index.d.ts",
|
||||
"import": "./dist/layouts/home/index.js"
|
||||
},
|
||||
"./layouts/home/not-found": {
|
||||
"types": "./dist/layouts/home/not-found.d.ts",
|
||||
"import": "./dist/layouts/home/not-found.js"
|
||||
},
|
||||
"./layouts/home/navbar": {
|
||||
"types": "./dist/layouts/home/navbar.d.ts",
|
||||
"import": "./dist/layouts/home/navbar.js"
|
||||
},
|
||||
"./layouts/docs": {
|
||||
"types": "./dist/layouts/docs/index.d.ts",
|
||||
"import": "./dist/layouts/docs/index.js"
|
||||
},
|
||||
"./layouts/docs/page": {
|
||||
"types": "./dist/layouts/docs/page/index.d.ts",
|
||||
"import": "./dist/layouts/docs/page/index.js"
|
||||
},
|
||||
"./layouts/flux": {
|
||||
"types": "./dist/layouts/flux/index.d.ts",
|
||||
"import": "./dist/layouts/flux/index.js"
|
||||
},
|
||||
"./layouts/flux/page": {
|
||||
"types": "./dist/layouts/flux/page/index.d.ts",
|
||||
"import": "./dist/layouts/flux/page/index.js"
|
||||
},
|
||||
"./layouts/notebook": {
|
||||
"types": "./dist/layouts/notebook/index.d.ts",
|
||||
"import": "./dist/layouts/notebook/index.js"
|
||||
},
|
||||
"./layouts/notebook/page": {
|
||||
"types": "./dist/layouts/notebook/page/index.d.ts",
|
||||
"import": "./dist/layouts/notebook/page/index.js"
|
||||
},
|
||||
"./layouts/shared": {
|
||||
"types": "./dist/layouts/shared/index.d.ts",
|
||||
"import": "./dist/layouts/shared/index.js"
|
||||
},
|
||||
"./page": {
|
||||
"types": "./dist/page.d.ts",
|
||||
"import": "./dist/page.js"
|
||||
},
|
||||
"./provider/*": {
|
||||
"types": "./dist/provider/*.d.ts",
|
||||
"import": "./dist/provider/*.js"
|
||||
},
|
||||
"./contexts/*": {
|
||||
"types": "./dist/contexts/*.d.ts",
|
||||
"import": "./dist/contexts/*.js"
|
||||
},
|
||||
"./components/accordion": "./dist/components/accordion.js",
|
||||
"./components/banner": "./dist/components/banner.js",
|
||||
"./components/callout": "./dist/components/callout.js",
|
||||
"./components/card": "./dist/components/card.js",
|
||||
"./components/codeblock": "./dist/components/codeblock.js",
|
||||
"./components/codeblock.rsc": "./dist/components/codeblock.rsc.js",
|
||||
"./components/dialog/search": "./dist/components/dialog/search.js",
|
||||
"./components/dialog/search-algolia": "./dist/components/dialog/search-algolia.js",
|
||||
"./components/dialog/search-default": "./dist/components/dialog/search-default.js",
|
||||
"./components/dialog/search-orama": "./dist/components/dialog/search-orama.js",
|
||||
"./components/dynamic-codeblock": "./dist/components/dynamic-codeblock.js",
|
||||
"./components/dynamic-codeblock.core": "./dist/components/dynamic-codeblock.core.js",
|
||||
"./components/files": "./dist/components/files.js",
|
||||
"./components/github-info": "./dist/components/github-info.js",
|
||||
"./components/heading": "./dist/components/heading.js",
|
||||
"./components/image-zoom": "./dist/components/image-zoom.js",
|
||||
"./components/inline-toc": "./dist/components/inline-toc.js",
|
||||
"./components/sidebar/base": "./dist/components/sidebar/base.js",
|
||||
"./components/sidebar/link-item": "./dist/components/sidebar/link-item.js",
|
||||
"./components/sidebar/page-tree": "./dist/components/sidebar/page-tree.js",
|
||||
"./components/sidebar/tabs": "./dist/components/sidebar/tabs/index.js",
|
||||
"./components/sidebar/tabs/dropdown": "./dist/components/sidebar/tabs/dropdown.js",
|
||||
"./components/steps": "./dist/components/steps.js",
|
||||
"./components/tabs": "./dist/components/tabs.js",
|
||||
"./components/toc": "./dist/components/toc/index.js",
|
||||
"./components/toc/clerk": "./dist/components/toc/clerk.js",
|
||||
"./components/toc/default": "./dist/components/toc/default.js",
|
||||
"./components/type-table": "./dist/components/type-table.js",
|
||||
"./components/ui/accordion": "./dist/components/ui/accordion.js",
|
||||
"./components/ui/button": "./dist/components/ui/button.js",
|
||||
"./components/ui/collapsible": "./dist/components/ui/collapsible.js",
|
||||
"./components/ui/navigation-menu": "./dist/components/ui/navigation-menu.js",
|
||||
"./components/ui/popover": "./dist/components/ui/popover.js",
|
||||
"./components/ui/scroll-area": "./dist/components/ui/scroll-area.js",
|
||||
"./components/ui/tabs": "./dist/components/ui/tabs.js",
|
||||
"./contexts/i18n": "./dist/contexts/i18n.js",
|
||||
"./contexts/search": "./dist/contexts/search.js",
|
||||
"./contexts/tree": "./dist/contexts/tree.js",
|
||||
"./i18n": "./dist/i18n.js",
|
||||
"./layouts/docs": "./dist/layouts/docs/index.js",
|
||||
"./layouts/docs/page": "./dist/layouts/docs/page/index.js",
|
||||
"./layouts/docs/page/slots/breadcrumb": "./dist/layouts/docs/page/slots/breadcrumb.js",
|
||||
"./layouts/docs/page/slots/container": "./dist/layouts/docs/page/slots/container.js",
|
||||
"./layouts/docs/page/slots/footer": "./dist/layouts/docs/page/slots/footer.js",
|
||||
"./layouts/docs/page/slots/toc": "./dist/layouts/docs/page/slots/toc.js",
|
||||
"./layouts/docs/page/slots/toc-popover": "./dist/layouts/docs/page/slots/toc-popover.js",
|
||||
"./layouts/docs/slots/container": "./dist/layouts/docs/slots/container.js",
|
||||
"./layouts/docs/slots/header": "./dist/layouts/docs/slots/header.js",
|
||||
"./layouts/docs/slots/sidebar": "./dist/layouts/docs/slots/sidebar.js",
|
||||
"./layouts/flux": "./dist/layouts/flux/index.js",
|
||||
"./layouts/flux/page": "./dist/layouts/flux/page/index.js",
|
||||
"./layouts/flux/page/slots/breadcrumb": "./dist/layouts/flux/page/slots/breadcrumb.js",
|
||||
"./layouts/flux/page/slots/container": "./dist/layouts/flux/page/slots/container.js",
|
||||
"./layouts/flux/page/slots/footer": "./dist/layouts/flux/page/slots/footer.js",
|
||||
"./layouts/flux/page/slots/toc": "./dist/layouts/flux/page/slots/toc.js",
|
||||
"./layouts/flux/slots/container": "./dist/layouts/flux/slots/container.js",
|
||||
"./layouts/flux/slots/sidebar": "./dist/layouts/flux/slots/sidebar.js",
|
||||
"./layouts/flux/slots/tab-dropdown": "./dist/layouts/flux/slots/tab-dropdown.js",
|
||||
"./layouts/home": "./dist/layouts/home/index.js",
|
||||
"./layouts/home/navbar": "./dist/layouts/home/navbar.js",
|
||||
"./layouts/home/not-found": "./dist/layouts/home/not-found.js",
|
||||
"./layouts/home/slots/container": "./dist/layouts/home/slots/container.js",
|
||||
"./layouts/home/slots/header": "./dist/layouts/home/slots/header.js",
|
||||
"./layouts/notebook": "./dist/layouts/notebook/index.js",
|
||||
"./layouts/notebook/page": "./dist/layouts/notebook/page/index.js",
|
||||
"./layouts/notebook/page/slots/breadcrumb": "./dist/layouts/notebook/page/slots/breadcrumb.js",
|
||||
"./layouts/notebook/page/slots/container": "./dist/layouts/notebook/page/slots/container.js",
|
||||
"./layouts/notebook/page/slots/footer": "./dist/layouts/notebook/page/slots/footer.js",
|
||||
"./layouts/notebook/page/slots/toc": "./dist/layouts/notebook/page/slots/toc.js",
|
||||
"./layouts/notebook/page/slots/toc-popover": "./dist/layouts/notebook/page/slots/toc-popover.js",
|
||||
"./layouts/notebook/slots/container": "./dist/layouts/notebook/slots/container.js",
|
||||
"./layouts/notebook/slots/header": "./dist/layouts/notebook/slots/header.js",
|
||||
"./layouts/notebook/slots/sidebar": "./dist/layouts/notebook/slots/sidebar.js",
|
||||
"./layouts/shared": "./dist/layouts/shared/index.js",
|
||||
"./layouts/shared/slots/language-select": "./dist/layouts/shared/slots/language-select.js",
|
||||
"./layouts/shared/slots/search-trigger": "./dist/layouts/shared/slots/search-trigger.js",
|
||||
"./layouts/shared/slots/theme-switch": "./dist/layouts/shared/slots/theme-switch.js",
|
||||
"./mdx": {
|
||||
"types": "./dist/mdx.d.ts",
|
||||
"node": "./dist/mdx.server.js",
|
||||
"import": "./dist/mdx.js"
|
||||
},
|
||||
"./og": {
|
||||
"types": "./dist/og.d.ts",
|
||||
"import": "./dist/og.js"
|
||||
},
|
||||
"./og/*": {
|
||||
"types": "./dist/og/*.d.ts",
|
||||
"import": "./dist/og/*.js"
|
||||
},
|
||||
"./og": "./dist/og.js",
|
||||
"./og/takumi": "./dist/og/takumi.js",
|
||||
"./page": "./dist/page.js",
|
||||
"./provider/base": "./dist/provider/base.js",
|
||||
"./provider/next": "./dist/provider/next.js",
|
||||
"./provider/react-router": "./dist/provider/react-router.js",
|
||||
"./provider/tanstack": "./dist/provider/tanstack.js",
|
||||
"./provider/waku": "./dist/provider/waku.js",
|
||||
"./utils/use-copy-button": "./dist/utils/use-copy-button.js",
|
||||
"./utils/use-footer-items": "./dist/utils/use-footer-items.js",
|
||||
"./utils/use-is-scroll-top": "./dist/utils/use-is-scroll-top.js",
|
||||
"./package.json": "./package.json",
|
||||
"./style.css": "./dist/style.css",
|
||||
"./css/*": "./css/*",
|
||||
"./image-zoom.css": "./dist/image-zoom.css",
|
||||
"./utils/get-sidebar-tabs": {
|
||||
"types": "./dist/components/sidebar/tabs/index.d.ts",
|
||||
"import": "./dist/components/sidebar/tabs/index.js"
|
||||
},
|
||||
"./utils/*": {
|
||||
"types": "./dist/utils/*.d.ts",
|
||||
"import": "./dist/utils/*.js"
|
||||
"./components/tabs.unstyled": {
|
||||
"types": "./dist/components/ui/tabs.d.ts",
|
||||
"import": "./dist/components/ui/tabs.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -22,6 +22,7 @@ export const registry: Registry = {
|
||||
...(await findSlotComponents(dir)),
|
||||
{
|
||||
name: 'layouts/sidebar',
|
||||
unlisted: true,
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
@@ -57,7 +58,12 @@ export const registry: Registry = {
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/shared/index.tsx',
|
||||
target: '<dir>/layout/shared.tsx',
|
||||
target: '<dir>/layout/shared/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/shared/client.tsx',
|
||||
target: '<dir>/layout/shared/client.tsx',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -327,7 +333,7 @@ export const registry: Registry = {
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/slots/page-actions.tsx',
|
||||
path: 'layouts/shared/page-actions.tsx',
|
||||
target: '<dir>/ai/page-actions.tsx',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type * as Base from './base';
|
||||
import { useLinkItemActive, type LinkItemType } from '@/utils/link-item';
|
||||
import { isLinkItemActive, type LinkItemType } from '@/layouts/shared';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
|
||||
type InternalComponents = Pick<
|
||||
typeof Base,
|
||||
@@ -28,7 +29,8 @@ export function createLinkItemRenderer({
|
||||
}: HTMLAttributes<HTMLElement> & {
|
||||
item: Exclude<LinkItemType, { type: 'icon' }>;
|
||||
}) {
|
||||
const active = useLinkItemActive(item);
|
||||
const pathname = usePathname();
|
||||
const active = isLinkItemActive(item, pathname);
|
||||
if (item.type === 'custom') return <div {...props}>{item.children}</div>;
|
||||
|
||||
if (item.type === 'menu')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cn } from '@/utils/cn';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import { useIsScrollTop } from '@/utils/use-is-scroll-top';
|
||||
import type { LinkItemType } from '@/utils/link-item';
|
||||
import type { LinkItemType } from '@/layouts/shared';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarProvider,
|
||||
@@ -44,11 +44,13 @@ const { useProvider } = baseSlots({
|
||||
},
|
||||
});
|
||||
|
||||
type SlotsProps = BaseSlotsProps & Pick<DocsLayoutProps, 'tabMode'>;
|
||||
interface SlotsProps extends BaseSlotsProps<DocsLayoutProps> {
|
||||
tabs: LayoutTab[];
|
||||
tabMode: NonNullable<DocsLayoutProps['tabMode']>;
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<{
|
||||
props: SlotsProps;
|
||||
tabs: LayoutTab[];
|
||||
isNavTransparent: boolean;
|
||||
navItems: LinkItemType[];
|
||||
menuItems: LinkItemType[];
|
||||
@@ -131,9 +133,9 @@ export function LayoutBody(
|
||||
value={{
|
||||
props: {
|
||||
tabMode,
|
||||
tabs,
|
||||
...baseProps,
|
||||
},
|
||||
tabs,
|
||||
isNavTransparent,
|
||||
slots,
|
||||
...linkItems,
|
||||
|
||||
@@ -36,9 +36,9 @@ interface SidebarOptions extends SidebarProps, SidebarProviderProps {
|
||||
|
||||
export function DocsLayout({
|
||||
tree,
|
||||
sidebar: { tabs: defaultTabs, tabMode: defaultTabMode, ...sidebarProps } = {},
|
||||
tabs: layoutTabs = defaultTabs,
|
||||
tabMode = defaultTabMode,
|
||||
sidebar: { tabs: _tabs, tabMode: _tabMode, ...sidebarProps } = {},
|
||||
tabs: layoutTabs = _tabs,
|
||||
tabMode = _tabMode,
|
||||
children,
|
||||
...props
|
||||
}: DocsLayoutProps) {
|
||||
|
||||
@@ -77,10 +77,11 @@ export interface DocsPageSlots {
|
||||
breadcrumb?: FC<BreadcrumbProps>;
|
||||
}
|
||||
|
||||
type PageSlotsProps = Pick<
|
||||
DocsPageProps,
|
||||
'full' | 'breadcrumb' | 'footer' | 'tableOfContent' | 'tableOfContentPopover'
|
||||
>;
|
||||
interface PageSlotsProps extends Pick<DocsPageProps, 'full' | 'breadcrumb' | 'footer'> {
|
||||
tableOfContent: TOCProps & { component?: ReactNode };
|
||||
tableOfContentPopover: TOCPopoverProps & { component?: ReactNode };
|
||||
}
|
||||
|
||||
const PageContext = createContext<{
|
||||
props: PageSlotsProps;
|
||||
slots: DocsPageSlots;
|
||||
@@ -122,7 +123,13 @@ export function DocsPage({
|
||||
let content = (
|
||||
<>
|
||||
{slots.tocPopover && <slots.tocPopover />}
|
||||
{slots.container && <slots.container {...containerProps}>{children}</slots.container>}
|
||||
{slots.container && (
|
||||
<slots.container {...containerProps}>
|
||||
{slots.breadcrumb && <slots.breadcrumb />}
|
||||
{children}
|
||||
{slots.footer && <slots.footer />}
|
||||
</slots.container>
|
||||
)}
|
||||
{slots.toc && <slots.toc />}
|
||||
</>
|
||||
);
|
||||
@@ -153,27 +160,27 @@ export function DocsPage({
|
||||
}
|
||||
|
||||
function InlineBreadcrumb(props: BreadcrumbProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.breadcrumb ?? {};
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props.breadcrumb ?? {};
|
||||
if (component) return component;
|
||||
return <Breadcrumb {...props} {...rest} />;
|
||||
}
|
||||
|
||||
function InlineFooter(props: FooterProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.footer ?? {};
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props.footer ?? {};
|
||||
if (component) return component;
|
||||
return <Footer {...props} {...rest} />;
|
||||
}
|
||||
|
||||
function InlineTOCPopover(props: TOCPopoverProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.tableOfContentPopover ?? {};
|
||||
if (component) return component;
|
||||
return <TOCPopover {...props} {...rest} />;
|
||||
const { tableOfContentPopover } = useDocsPage().props;
|
||||
if (tableOfContentPopover.component) return tableOfContentPopover.component;
|
||||
return <TOCPopover {...props} {...tableOfContentPopover} />;
|
||||
}
|
||||
|
||||
function InlineTOC(props: TOCProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.tableOfContent ?? {};
|
||||
if (component) return component;
|
||||
return <TOC {...props} {...rest} />;
|
||||
const { tableOfContent } = useDocsPage().props;
|
||||
if (tableOfContent.component) return tableOfContent.component;
|
||||
return <TOC {...props} {...tableOfContent} />;
|
||||
}
|
||||
|
||||
export function PageLastUpdate({
|
||||
|
||||
@@ -66,8 +66,6 @@ export {
|
||||
PageLastUpdate,
|
||||
useDocsPage,
|
||||
} from './client';
|
||||
export { TOC, type TOCProps } from './slots/toc';
|
||||
export { TOCPopover, type TOCPopoverProps } from './slots/toc-popover';
|
||||
export { type BreadcrumbProps, Breadcrumb as PageBreadcrumb } from './slots/breadcrumb';
|
||||
export { type FooterProps, Footer as PageFooter } from './slots/footer';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/slots/page-actions';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/shared/page-actions';
|
||||
|
||||
@@ -4,7 +4,6 @@ import { cn } from '@/utils/cn';
|
||||
|
||||
export function Container(props: ComponentProps<'article'>) {
|
||||
const {
|
||||
slots,
|
||||
props: { full },
|
||||
} = useDocsPage();
|
||||
|
||||
@@ -19,9 +18,7 @@ export function Container(props: ComponentProps<'article'>) {
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{slots.breadcrumb && <slots.breadcrumb />}
|
||||
{props.children}
|
||||
{slots.footer && <slots.footer />}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import * as Base from '@/components/sidebar/base';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type ComponentProps, type ReactNode, useRef } from 'react';
|
||||
import { type ComponentProps, type ReactNode, useMemo, useRef, useState } from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import {
|
||||
createPageTreeRenderer,
|
||||
@@ -9,13 +9,16 @@ import {
|
||||
} from '@/components/sidebar/page-tree';
|
||||
import { createLinkItemRenderer } from '@/components/sidebar/link-item';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { SearchTrigger } from '@/layouts/slots/search-toggle';
|
||||
import { Languages, Sidebar as SidebarIcon } from 'lucide-react';
|
||||
import { SearchTrigger } from '@/layouts/shared/slots/search-trigger';
|
||||
import { Check, ChevronsUpDown, Languages, Sidebar as SidebarIcon } from 'lucide-react';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
import { ScrollArea, ScrollAreaProps, ScrollViewport } from '@/components/ui/scroll-area';
|
||||
import { SidebarTabsDropdown } from '@/components/sidebar/tabs/dropdown';
|
||||
import { useDocsLayout } from '../client';
|
||||
import { LinkItem } from '@/utils/link-item';
|
||||
import { LinkItem } from '@/layouts/shared';
|
||||
import { isLayoutTabActive, type LayoutTab } from '@/layouts/shared';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import Link from 'fumadocs-core/link';
|
||||
|
||||
const itemVariants = cva(
|
||||
'relative flex flex-row items-center gap-2 rounded-lg p-2 text-start text-fd-muted-foreground wrap-anywhere [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
@@ -56,10 +59,9 @@ export function SidebarProvider(props: SidebarProviderProps) {
|
||||
|
||||
export function Sidebar({ footer, banner, collapsible = true, components, ...rest }: SidebarProps) {
|
||||
const {
|
||||
tabs,
|
||||
menuItems,
|
||||
slots,
|
||||
props: { nav, tabMode = 'auto' },
|
||||
props: { tabs, nav, tabMode },
|
||||
} = useDocsLayout();
|
||||
const iconLinks = menuItems.filter((item) => item.type === 'icon');
|
||||
const viewport = (
|
||||
@@ -97,7 +99,7 @@ export function Sidebar({ footer, banner, collapsible = true, components, ...res
|
||||
)}
|
||||
</div>
|
||||
{slots.searchTrigger && <slots.searchTrigger.full hideIfDisabled />}
|
||||
{tabs.length > 0 && tabMode === 'auto' && <SidebarTabsDropdown options={tabs} />}
|
||||
{tabs.length > 0 && tabMode === 'auto' && <SidebarTabsDropdown tabs={tabs} />}
|
||||
{banner}
|
||||
</div>
|
||||
{viewport}
|
||||
@@ -165,7 +167,7 @@ export function Sidebar({ footer, banner, collapsible = true, components, ...res
|
||||
<SidebarIcon />
|
||||
</SidebarTrigger>
|
||||
</div>
|
||||
{tabs.length > 0 && <SidebarTabsDropdown options={tabs} />}
|
||||
{tabs.length > 0 && <SidebarTabsDropdown tabs={tabs} />}
|
||||
{banner}
|
||||
</div>
|
||||
{viewport}
|
||||
@@ -395,6 +397,93 @@ function SidebarFolderContent({
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTabsDropdown({
|
||||
tabs,
|
||||
placeholder,
|
||||
...props
|
||||
}: {
|
||||
placeholder?: ReactNode;
|
||||
tabs: LayoutTab[];
|
||||
} & ComponentProps<'button'>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { closeOnRedirect } = useSidebar();
|
||||
const pathname = usePathname();
|
||||
|
||||
const selected = useMemo(() => {
|
||||
return tabs.findLast((item) => isLayoutTabActive(item, pathname));
|
||||
}, [tabs, pathname]);
|
||||
|
||||
const onClick = () => {
|
||||
closeOnRedirect.current = false;
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const item = selected ? (
|
||||
<>
|
||||
<div className="size-9 shrink-0 empty:hidden md:size-5">{selected.icon}</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{selected.title}</p>
|
||||
<p className="text-sm text-fd-muted-foreground empty:hidden md:hidden">
|
||||
{selected.description}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
placeholder
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
{item && (
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-2 border bg-fd-secondary/50 text-start text-fd-secondary-foreground transition-colors hover:bg-fd-accent data-[state=open]:bg-fd-accent data-[state=open]:text-fd-accent-foreground',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
<ChevronsUpDown className="shrink-0 ms-auto size-4 text-fd-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
<PopoverContent className="flex flex-col gap-1 w-(--radix-popover-trigger-width) p-1 fd-scroll-container">
|
||||
{tabs.map((item) => {
|
||||
const isActive = selected && item.url === selected.url;
|
||||
if (!isActive && item.unlisted) return;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.url}
|
||||
href={item.url}
|
||||
onClick={onClick}
|
||||
{...item.props}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-1.5 hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||
item.props?.className,
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 size-9 md:mb-auto md:size-5 empty:hidden">{item.icon}</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">{item.title}</p>
|
||||
<p className="text-[0.8125rem] text-fd-muted-foreground mt-1 empty:hidden">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Check
|
||||
className={cn(
|
||||
'shrink-0 ms-auto size-3.5 text-fd-primary',
|
||||
!isActive && 'invisible',
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function getItemOffset(depth: number) {
|
||||
return `calc(${2 + 3 * depth} * var(--spacing))`;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { cn } from '@/utils/cn';
|
||||
import { TabDropdown, type TabDropdownProps } from './slots/tab-dropdown';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { LinkItem, type LinkItemType } from '@/utils/link-item';
|
||||
import { LinkItem, type LinkItemType } from '@/layouts/shared';
|
||||
import { motion } from 'motion/react';
|
||||
import { RemoveScroll } from 'react-remove-scroll';
|
||||
import { useSearchContext } from '@/contexts/search';
|
||||
@@ -87,12 +87,12 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
nav = {},
|
||||
sidebar: {
|
||||
enabled: sidebarEnabled = true,
|
||||
tabs: sidebarTabs,
|
||||
tabs: _tabs,
|
||||
defaultOpenLevel,
|
||||
prefetch,
|
||||
...sidebarProps
|
||||
} = {},
|
||||
tabs: defaultTabs = sidebarTabs,
|
||||
tabs: defaultTabs = _tabs,
|
||||
children,
|
||||
containerProps,
|
||||
renderNavigationPanel = (props) => <NavigationPanel {...props} />,
|
||||
@@ -113,7 +113,6 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
}
|
||||
return [];
|
||||
}, [tree, defaultTabs]);
|
||||
const iconLinks = linkItems.menuItems.filter((item) => item.type === 'icon');
|
||||
const slots: DocsSlots = {
|
||||
...baseSlots,
|
||||
container: defaultSlots.container ?? Container,
|
||||
@@ -193,16 +192,18 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
)}
|
||||
</>
|
||||
),
|
||||
link: iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(buttonVariants({ size: 'icon-sm', color: 'ghost' }))}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
)),
|
||||
link: linkItems.menuItems
|
||||
.filter((item) => item.type === 'icon')
|
||||
.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(buttonVariants({ size: 'icon-sm', color: 'ghost' }))}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
)),
|
||||
})}
|
||||
</TreeContextProvider>
|
||||
</LayoutContext>
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { type ComponentProps, createContext, type FC, use, useEffect, useState } from 'react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
use,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import { TOC, type TOCProps } from './slots/toc';
|
||||
import { Footer, type FooterProps } from './slots/footer';
|
||||
import { Breadcrumb, type BreadcrumbProps } from './slots/breadcrumb';
|
||||
import type { DocsPageProps } from '.';
|
||||
import { TOCProvider, type TOCProviderProps } from '@/components/toc';
|
||||
import { Container } from './slots/container';
|
||||
import type { TOCItemType } from 'fumadocs-core/toc';
|
||||
|
||||
export interface DocsPageSlots {
|
||||
toc?: FC<TOCProps>;
|
||||
@@ -18,7 +26,51 @@ export interface DocsPageSlots {
|
||||
breadcrumb?: FC<BreadcrumbProps>;
|
||||
}
|
||||
|
||||
type PageSlotsProps = Pick<DocsPageProps, 'full' | 'breadcrumb' | 'footer' | 'tableOfContent'>;
|
||||
export interface DocsPageProps extends ComponentProps<'article'> {
|
||||
toc?: TOCItemType[];
|
||||
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
children?: ReactNode;
|
||||
slots?: DocsPageSlots;
|
||||
|
||||
footer?: FooterOptions;
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
}
|
||||
|
||||
interface TableOfContentOptions extends Pick<TOCProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.toc` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.breadcrumb` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.footer` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface PageSlotsProps extends Pick<DocsPageProps, 'full' | 'footer' | 'breadcrumb'> {
|
||||
tableOfContent: TOCProps & { component?: ReactNode };
|
||||
}
|
||||
|
||||
const PageContext = createContext<{
|
||||
props: PageSlotsProps;
|
||||
slots: DocsPageSlots;
|
||||
@@ -57,7 +109,13 @@ export function DocsPage({
|
||||
let content = (
|
||||
<>
|
||||
{slots.toc && <slots.toc />}
|
||||
{slots.container && <slots.container {...containerProps}>{children}</slots.container>}
|
||||
{slots.container && (
|
||||
<slots.container {...containerProps}>
|
||||
{slots.breadcrumb && <slots.breadcrumb />}
|
||||
{children}
|
||||
{slots.footer && <slots.footer />}
|
||||
</slots.container>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -86,21 +144,21 @@ export function DocsPage({
|
||||
}
|
||||
|
||||
function InlineBreadcrumb(props: BreadcrumbProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.breadcrumb ?? {};
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props.breadcrumb ?? {};
|
||||
if (component) return component;
|
||||
return <Breadcrumb {...props} {...rest} />;
|
||||
}
|
||||
|
||||
function InlineFooter(props: FooterProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.footer ?? {};
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props.footer ?? {};
|
||||
if (component) return component;
|
||||
return <Footer {...props} {...rest} />;
|
||||
}
|
||||
|
||||
function InlineTOC(props: TOCProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.tableOfContent ?? {};
|
||||
if (component) return component;
|
||||
return <TOC {...props} {...rest} />;
|
||||
const { tableOfContent } = useDocsPage().props;
|
||||
if (tableOfContent.component) return tableOfContent.component;
|
||||
return <TOC {...props} {...tableOfContent} />;
|
||||
}
|
||||
|
||||
export function PageLastUpdate({
|
||||
|
||||
@@ -1,54 +1,8 @@
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Edit } from 'lucide-react';
|
||||
import { I18nLabel } from '@/contexts/i18n';
|
||||
import type { AnchorProviderProps, TOCItemType } from 'fumadocs-core/toc';
|
||||
import type { DocsPageSlots } from './client';
|
||||
import type { TOCProps } from './slots/toc';
|
||||
import type { BreadcrumbProps } from './slots/breadcrumb';
|
||||
import type { FooterProps } from './slots/footer';
|
||||
|
||||
export interface DocsPageProps extends ComponentProps<'article'> {
|
||||
toc?: TOCItemType[];
|
||||
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
children?: ReactNode;
|
||||
slots?: DocsPageSlots;
|
||||
|
||||
footer?: FooterOptions;
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
}
|
||||
|
||||
interface TableOfContentOptions extends Pick<AnchorProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.toc` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.breadcrumb` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.footer` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
export function EditOnGitHub(props: ComponentProps<'a'>) {
|
||||
return (
|
||||
@@ -105,8 +59,13 @@ export function DocsTitle({ children, className, ...props }: ComponentProps<'h1'
|
||||
);
|
||||
}
|
||||
|
||||
export { DocsPage, type DocsPageSlots, PageLastUpdate, useDocsPage } from './client';
|
||||
export { TOC, type TOCProps } from './slots/toc';
|
||||
export {
|
||||
DocsPage,
|
||||
type DocsPageSlots,
|
||||
type DocsPageProps,
|
||||
PageLastUpdate,
|
||||
useDocsPage,
|
||||
} from './client';
|
||||
export { type FooterProps, Footer as PageFooter } from './slots/footer';
|
||||
export { type BreadcrumbProps, Breadcrumb as PageBreadcrumb } from './slots/breadcrumb';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/slots/page-actions';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/shared/page-actions';
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useDocsPage } from '../client';
|
||||
|
||||
export function Container(props: ComponentProps<'article'>) {
|
||||
const {
|
||||
slots,
|
||||
props: { full },
|
||||
} = useDocsPage();
|
||||
|
||||
@@ -19,9 +18,7 @@ export function Container(props: ComponentProps<'article'>) {
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{slots.breadcrumb && <slots.breadcrumb />}
|
||||
{props.children}
|
||||
{slots.footer && <slots.footer />}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { BaseLayoutProps, NavOptions } from '@/layouts/shared';
|
||||
import { type ComponentProps, createContext, type FC, use } from 'react';
|
||||
import { baseSlots, useLinkItems, type BaseSlots, type BaseSlotsProps } from '@/layouts/shared';
|
||||
import type { LinkItemType } from '@/utils/link-item';
|
||||
import type { LinkItemType } from '@/layouts/shared';
|
||||
import { Container } from './slots/container';
|
||||
import { Header } from './slots/header';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { type ComponentProps, Fragment, useState } from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type LinkItemType, LinkItem } from '@/utils/link-item';
|
||||
import { type LinkItemType, LinkItem } from '@/layouts/shared';
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
import { type ComponentProps, createContext, type FC, use } from 'react';
|
||||
import { type DocsLayoutProps } from '.';
|
||||
import { useIsScrollTop } from '@/utils/use-is-scroll-top';
|
||||
import { type LinkItemType } from '@/utils/link-item';
|
||||
import { baseSlots, type BaseSlots, type BaseSlotsProps, LayoutTab, useLinkItems } from '../shared';
|
||||
import { type LinkItemType } from '@/layouts/shared';
|
||||
import {
|
||||
baseSlots,
|
||||
type BaseSlots,
|
||||
type BaseSlotsProps,
|
||||
type LayoutTab,
|
||||
useLinkItems,
|
||||
} from '../shared';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { Container } from './slots/container';
|
||||
import {
|
||||
@@ -35,18 +41,18 @@ const { useProvider } = baseSlots({
|
||||
},
|
||||
});
|
||||
|
||||
type SlotsProps = BaseSlotsProps;
|
||||
interface SlotsProps extends BaseSlotsProps<DocsLayoutProps> {
|
||||
sidebar: SidebarProps;
|
||||
tabMode: NonNullable<DocsLayoutProps['tabMode']>;
|
||||
tabs: LayoutTab[];
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<{
|
||||
props: SlotsProps;
|
||||
tabs: LayoutTab[];
|
||||
sidebarCollapsible: boolean;
|
||||
isNavTransparent: boolean;
|
||||
navItems: LinkItemType[];
|
||||
menuItems: LinkItemType[];
|
||||
slots: DocsSlots;
|
||||
tabMode: 'sidebar' | 'navbar';
|
||||
navMode: 'top' | 'auto';
|
||||
} | null>(null);
|
||||
|
||||
export function useNotebookLayout() {
|
||||
@@ -64,11 +70,7 @@ export function LayoutBody(
|
||||
},
|
||||
) {
|
||||
const {
|
||||
nav: {
|
||||
enabled: navEnabled = true,
|
||||
mode: navMode = 'auto',
|
||||
transparentMode: navTransparentMode = 'none',
|
||||
} = {},
|
||||
nav: { enabled: navEnabled = true, transparentMode: navTransparentMode = 'none' } = {},
|
||||
sidebar: { defaultOpenLevel, prefetch, ...sidebarProps } = {},
|
||||
slots: defaultSlots,
|
||||
tabMode = 'sidebar',
|
||||
@@ -118,11 +120,12 @@ export function LayoutBody(
|
||||
<TreeContextProvider tree={tree}>
|
||||
<LayoutContext
|
||||
value={{
|
||||
sidebarCollapsible: sidebarProps.collapsible ?? true,
|
||||
props: baseProps,
|
||||
tabMode,
|
||||
navMode,
|
||||
tabs,
|
||||
props: {
|
||||
tabs,
|
||||
tabMode,
|
||||
sidebar: sidebarProps,
|
||||
...baseProps,
|
||||
},
|
||||
isNavTransparent,
|
||||
slots,
|
||||
...linkItems,
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { type ComponentProps, createContext, type FC, use, useEffect, useState } from 'react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
use,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import { TOC, type TOCProps } from './slots/toc';
|
||||
import { TOCPopover, type TOCPopoverProps } from './slots/toc-popover';
|
||||
import { Footer, type FooterProps } from './slots/footer';
|
||||
import { Breadcrumb, type BreadcrumbProps } from './slots/breadcrumb';
|
||||
import type { DocsPageProps } from '.';
|
||||
import { TOCProvider, type TOCProviderProps } from '@/components/toc';
|
||||
import { Container } from './slots/container';
|
||||
import type { TOCItemType } from 'fumadocs-core/toc';
|
||||
|
||||
export interface DocsPageSlots {
|
||||
toc?: FC<TOCProps>;
|
||||
@@ -20,10 +28,59 @@ export interface DocsPageSlots {
|
||||
breadcrumb?: FC<BreadcrumbProps>;
|
||||
}
|
||||
|
||||
type PageSlotsProps = Pick<
|
||||
DocsPageProps,
|
||||
'full' | 'breadcrumb' | 'footer' | 'tableOfContent' | 'tableOfContentPopover'
|
||||
>;
|
||||
export interface DocsPageProps extends ComponentProps<'article'> {
|
||||
toc?: TOCItemType[];
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
slots?: DocsPageSlots;
|
||||
|
||||
footer?: FooterOptions;
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
tableOfContentPopover?: TableOfContentPopoverOptions;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.breadcrumb` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.footer` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface TableOfContentOptions extends Pick<TOCProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.toc` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface TableOfContentPopoverOptions extends TOCPopoverProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.tocPopover` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface PageSlotsProps extends Pick<DocsPageProps, 'full' | 'breadcrumb' | 'footer'> {
|
||||
tableOfContent: TOCProps & { component?: ReactNode };
|
||||
tableOfContentPopover: TOCPopoverProps & { component?: ReactNode };
|
||||
}
|
||||
|
||||
const PageContext = createContext<{
|
||||
props: PageSlotsProps;
|
||||
slots: DocsPageSlots;
|
||||
@@ -65,7 +122,13 @@ export function DocsPage({
|
||||
let content = (
|
||||
<>
|
||||
{slots.tocPopover && <slots.tocPopover />}
|
||||
{slots.container && <slots.container {...containerProps}>{children}</slots.container>}
|
||||
{slots.container && (
|
||||
<slots.container {...containerProps}>
|
||||
{slots.breadcrumb && <slots.breadcrumb />}
|
||||
{children}
|
||||
{slots.footer && <slots.footer />}
|
||||
</slots.container>
|
||||
)}
|
||||
{slots.toc && <slots.toc />}
|
||||
</>
|
||||
);
|
||||
@@ -96,27 +159,27 @@ export function DocsPage({
|
||||
}
|
||||
|
||||
function InlineBreadcrumb(props: BreadcrumbProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.breadcrumb ?? {};
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props.breadcrumb ?? {};
|
||||
if (component) return component;
|
||||
return <Breadcrumb {...props} {...rest} />;
|
||||
}
|
||||
|
||||
function InlineFooter(props: FooterProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.footer ?? {};
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props.footer ?? {};
|
||||
if (component) return component;
|
||||
return <Footer {...props} {...rest} />;
|
||||
}
|
||||
|
||||
function InlineTOCPopover(props: TOCPopoverProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.tableOfContentPopover ?? {};
|
||||
if (component) return component;
|
||||
return <TOCPopover {...props} {...rest} />;
|
||||
const { tableOfContentPopover } = useDocsPage().props;
|
||||
if (tableOfContentPopover.component) return tableOfContentPopover.component;
|
||||
return <TOCPopover {...props} {...tableOfContentPopover} />;
|
||||
}
|
||||
|
||||
function InlineTOC(props: TOCProps) {
|
||||
const { component, enabled: _, ...rest } = useDocsPage().props?.tableOfContent ?? {};
|
||||
if (component) return component;
|
||||
return <TOC {...props} {...rest} />;
|
||||
const { tableOfContent } = useDocsPage().props;
|
||||
if (tableOfContent.component) return tableOfContent.component;
|
||||
return <TOC {...props} {...tableOfContent} />;
|
||||
}
|
||||
|
||||
export function PageLastUpdate({
|
||||
|
||||
@@ -1,62 +1,8 @@
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Edit } from 'lucide-react';
|
||||
import { I18nLabel } from '@/contexts/i18n';
|
||||
import type { AnchorProviderProps, TOCItemType } from 'fumadocs-core/toc';
|
||||
import type { DocsPageSlots } from './client';
|
||||
import type { TOCProps } from './slots/toc';
|
||||
import type { TOCPopoverProps } from './slots/toc-popover';
|
||||
import type { BreadcrumbProps } from './slots/breadcrumb';
|
||||
import type { FooterProps } from './slots/footer';
|
||||
|
||||
export interface DocsPageProps extends ComponentProps<'article'> {
|
||||
toc?: TOCItemType[];
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
slots?: DocsPageSlots;
|
||||
|
||||
footer?: FooterOptions;
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
tableOfContentPopover?: TableOfContentPopoverOptions;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.breadcrumb` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.footer` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface TableOfContentOptions extends Pick<AnchorProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.toc` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface TableOfContentPopoverOptions extends TOCPopoverProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.tocPopover` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
export function EditOnGitHub(props: ComponentProps<'a'>) {
|
||||
return (
|
||||
@@ -113,9 +59,13 @@ export function DocsTitle({ children, className, ...props }: ComponentProps<'h1'
|
||||
);
|
||||
}
|
||||
|
||||
export { DocsPage, type DocsPageSlots, PageLastUpdate, useDocsPage } from './client';
|
||||
export { TOC, type TOCProps } from './slots/toc';
|
||||
export { TOCPopover, type TOCPopoverProps } from './slots/toc-popover';
|
||||
export {
|
||||
DocsPage,
|
||||
type DocsPageProps,
|
||||
type DocsPageSlots,
|
||||
PageLastUpdate,
|
||||
useDocsPage,
|
||||
} from './client';
|
||||
export { type BreadcrumbProps, Breadcrumb as PageBreadcrumb } from './slots/breadcrumb';
|
||||
export { type FooterProps, Footer as PageFooter } from './slots/footer';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/slots/page-actions';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/shared/page-actions';
|
||||
|
||||
@@ -4,7 +4,6 @@ import { cn } from '@/utils/cn';
|
||||
|
||||
export function Container(props: ComponentProps<'article'>) {
|
||||
const {
|
||||
slots,
|
||||
props: { full },
|
||||
} = useDocsPage();
|
||||
|
||||
@@ -19,9 +18,7 @@ export function Container(props: ComponentProps<'article'>) {
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{slots.breadcrumb && <slots.breadcrumb />}
|
||||
{props.children}
|
||||
{slots.footer && <slots.footer />}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import type { ComponentProps } from 'react';
|
||||
import { useNotebookLayout } from '../client';
|
||||
|
||||
export function Container(props: ComponentProps<'div'>) {
|
||||
const { navMode, slots } = useNotebookLayout();
|
||||
const {
|
||||
props: { nav },
|
||||
slots,
|
||||
} = useNotebookLayout();
|
||||
const pageCol =
|
||||
'calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-col) - var(--fd-toc-width))';
|
||||
const { collapsed } = slots.sidebar?.useSidebar?.() ?? {};
|
||||
@@ -16,7 +19,7 @@ export function Container(props: ComponentProps<'div'>) {
|
||||
style={
|
||||
{
|
||||
gridTemplate:
|
||||
navMode === 'top'
|
||||
nav?.mode === 'top'
|
||||
? `". header header header ."
|
||||
"sidebar sidebar toc-popover toc-popover ."
|
||||
"sidebar sidebar main toc ." 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, ${pageCol}) var(--fd-toc-width) minmax(min-content, 1fr)`
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'react';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { LinkItem, type LinkItemType, type MenuItemType } from '@/utils/link-item';
|
||||
import { LinkItem, type LinkItemType, type MenuItemType } from '@/layouts/shared';
|
||||
import { useNotebookLayout } from '../client';
|
||||
import { type LayoutTab, isLayoutTabActive } from '@/layouts/shared';
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
|
||||
@@ -21,15 +21,13 @@ import Link from 'fumadocs-core/link';
|
||||
export function Header(props: ComponentProps<'header'>) {
|
||||
const {
|
||||
slots,
|
||||
tabs,
|
||||
navItems,
|
||||
isNavTransparent,
|
||||
sidebarCollapsible,
|
||||
tabMode,
|
||||
navMode,
|
||||
props: { nav },
|
||||
props: { tabMode, nav, tabs, sidebar },
|
||||
} = useNotebookLayout();
|
||||
const { open } = slots.sidebar?.useSidebar?.() ?? {};
|
||||
const navMode = nav?.mode ?? 'auto';
|
||||
const sidebarCollapsible = sidebar.collapsible ?? true;
|
||||
const showLayoutTabs = tabMode === 'navbar' && tabs.length > 0;
|
||||
|
||||
if (nav?.component) return nav.component;
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
import * as Base from '@/components/sidebar/base';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type ComponentProps, createElement, type FC, type ReactNode, useRef } from 'react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createElement,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import {
|
||||
createPageTreeRenderer,
|
||||
@@ -12,10 +20,13 @@ import { createLinkItemRenderer } from '@/components/sidebar/link-item';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
import { ScrollArea, ScrollViewport } from '@/components/ui/scroll-area';
|
||||
import { SidebarTabsDropdown } from '@/components/sidebar/tabs/dropdown';
|
||||
import { LinkItem } from '@/utils/link-item';
|
||||
import { Languages, Sidebar as SidebarIcon, X } from 'lucide-react';
|
||||
import { LinkItem } from '@/layouts/shared';
|
||||
import { Check, ChevronsUpDown, Languages, Sidebar as SidebarIcon, X } from 'lucide-react';
|
||||
import { useNotebookLayout } from '../client';
|
||||
import { isLayoutTabActive, type LayoutTab } from '@/layouts/shared';
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
import Link from 'fumadocs-core/link';
|
||||
|
||||
const itemVariants = cva(
|
||||
'relative flex flex-row items-center gap-2 rounded-lg p-2 text-start text-fd-muted-foreground wrap-anywhere [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
@@ -84,7 +95,10 @@ function SidebarViewport(props: ComponentProps<typeof ScrollArea>) {
|
||||
}
|
||||
|
||||
function SidebarContent({ ref: refProp, className, children, ...props }: ComponentProps<'aside'>) {
|
||||
const { navMode } = useNotebookLayout();
|
||||
const {
|
||||
props: { nav },
|
||||
} = useNotebookLayout();
|
||||
const navMode = nav?.mode ?? 'auto';
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
return (
|
||||
@@ -281,13 +295,11 @@ const SidebarLinkItem = createLinkItemRenderer({
|
||||
|
||||
export function Sidebar({ banner, footer, components, collapsible = true, ...rest }: SidebarProps) {
|
||||
const {
|
||||
tabs,
|
||||
menuItems,
|
||||
slots,
|
||||
tabMode,
|
||||
navMode,
|
||||
props: { nav },
|
||||
props: { nav, tabs, tabMode },
|
||||
} = useNotebookLayout();
|
||||
const navMode = nav?.mode ?? 'auto';
|
||||
const iconLinks = menuItems.filter((item) => item.type === 'icon');
|
||||
|
||||
function renderHeader(props: ComponentProps<'div'>) {
|
||||
@@ -448,3 +460,90 @@ export function Sidebar({ banner, footer, components, collapsible = true, ...res
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarTabsDropdown({
|
||||
options,
|
||||
placeholder,
|
||||
...props
|
||||
}: {
|
||||
placeholder?: ReactNode;
|
||||
options: LayoutTab[];
|
||||
} & ComponentProps<'button'>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { closeOnRedirect } = useSidebar();
|
||||
const pathname = usePathname();
|
||||
|
||||
const selected = useMemo(() => {
|
||||
return options.findLast((item) => isLayoutTabActive(item, pathname));
|
||||
}, [options, pathname]);
|
||||
|
||||
const onClick = () => {
|
||||
closeOnRedirect.current = false;
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const item = selected ? (
|
||||
<>
|
||||
<div className="size-9 shrink-0 empty:hidden md:size-5">{selected.icon}</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{selected.title}</p>
|
||||
<p className="text-sm text-fd-muted-foreground empty:hidden md:hidden">
|
||||
{selected.description}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
placeholder
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
{item && (
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-2 border bg-fd-secondary/50 text-start text-fd-secondary-foreground transition-colors hover:bg-fd-accent data-[state=open]:bg-fd-accent data-[state=open]:text-fd-accent-foreground',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
<ChevronsUpDown className="shrink-0 ms-auto size-4 text-fd-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
<PopoverContent className="flex flex-col gap-1 w-(--radix-popover-trigger-width) p-1 fd-scroll-container">
|
||||
{options.map((item) => {
|
||||
const isActive = selected && item.url === selected.url;
|
||||
if (!isActive && item.unlisted) return;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.url}
|
||||
href={item.url}
|
||||
onClick={onClick}
|
||||
{...item.props}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-1.5 hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||
item.props?.className,
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 size-9 md:mb-auto md:size-5 empty:hidden">{item.icon}</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">{item.title}</p>
|
||||
<p className="text-[0.8125rem] text-fd-muted-foreground mt-1 empty:hidden">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Check
|
||||
className={cn(
|
||||
'shrink-0 ms-auto size-3.5 text-fd-primary',
|
||||
!isActive && 'invisible',
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import type { FC, ComponentProps } from 'react';
|
||||
import { isLinkItemActive, type BaseLayoutProps, type LinkItemType } from '.';
|
||||
import {
|
||||
type LanguageSelectProps,
|
||||
type LanguageSelectTextProps,
|
||||
LanguageSelect,
|
||||
LanguageSelectText,
|
||||
} from './slots/language-select';
|
||||
import {
|
||||
type SearchTriggerProps,
|
||||
type FullSearchTriggeProps,
|
||||
SearchTrigger,
|
||||
FullSearchTrigger,
|
||||
} from './slots/search-trigger';
|
||||
import { type ThemeSwitchProps, ThemeSwitch } from './slots/theme-switch';
|
||||
|
||||
export function LinkItem({
|
||||
ref,
|
||||
item,
|
||||
...props
|
||||
}: Omit<ComponentProps<'a'>, 'href'> & { item: Extract<LinkItemType, { url: string }> }) {
|
||||
const pathname = usePathname();
|
||||
const active = isLinkItemActive(item, pathname);
|
||||
|
||||
return (
|
||||
<Link ref={ref} href={item.url} external={item.external} {...props} data-active={active}>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export interface BaseSlots {
|
||||
navTitle?: FC<ComponentProps<'a'>>;
|
||||
searchTrigger?: {
|
||||
sm: FC<SearchTriggerProps>;
|
||||
full: FC<FullSearchTriggeProps>;
|
||||
};
|
||||
languageSelect?: {
|
||||
root: FC<LanguageSelectProps>;
|
||||
text: FC<LanguageSelectTextProps>;
|
||||
};
|
||||
themeSwitch?: FC<ThemeSwitchProps>;
|
||||
}
|
||||
|
||||
export interface BaseSlotsProps<P extends BaseLayoutProps = BaseLayoutProps> extends Pick<
|
||||
P,
|
||||
'nav'
|
||||
> {
|
||||
themeSwitch: Omit<NonNullable<P['themeSwitch']>, 'enabled'>;
|
||||
searchToggle: Omit<NonNullable<P['searchToggle']>, 'enabled'>;
|
||||
}
|
||||
|
||||
export function baseSlots({ useProps }: { useProps: () => BaseSlotsProps }) {
|
||||
function InlineThemeSwitch(props: ThemeSwitchProps) {
|
||||
const { themeSwitch } = useProps();
|
||||
if (themeSwitch.component) return themeSwitch.component;
|
||||
return <ThemeSwitch {...props} {...themeSwitch} />;
|
||||
}
|
||||
|
||||
function InlineSearchTrigger(props: SearchTriggerProps) {
|
||||
const { searchToggle } = useProps();
|
||||
if (searchToggle.components?.sm) return searchToggle.components.sm;
|
||||
return <SearchTrigger {...props} {...searchToggle.sm} />;
|
||||
}
|
||||
|
||||
function InlineSearchTriggerFull(props: FullSearchTriggeProps) {
|
||||
const { searchToggle } = useProps();
|
||||
if (searchToggle.components?.lg) return searchToggle.components.lg;
|
||||
return <FullSearchTrigger {...props} {...searchToggle.full} />;
|
||||
}
|
||||
|
||||
function InlineNavTitle({ href: defaultUrl = '/', ...props }: ComponentProps<'a'>) {
|
||||
const { url = defaultUrl, title } = useProps().nav ?? {};
|
||||
|
||||
if (typeof title === 'function') return title({ href: url, ...props });
|
||||
return (
|
||||
<Link href={url} {...props}>
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
useProvider(options: BaseLayoutProps): {
|
||||
baseSlots: BaseSlots;
|
||||
baseProps: BaseSlotsProps;
|
||||
} {
|
||||
const { locales = [] } = useI18n();
|
||||
const {
|
||||
nav,
|
||||
slots = {},
|
||||
i18n = locales.length > 1,
|
||||
searchToggle: { enabled: searchToggleEnabled = true, ...searchToggle } = {},
|
||||
themeSwitch: { enabled: themeSwitchEnabled = true, ...themeSwitch } = {},
|
||||
} = options;
|
||||
|
||||
return {
|
||||
baseSlots: {
|
||||
navTitle: slots.navTitle ?? InlineNavTitle,
|
||||
themeSwitch: themeSwitchEnabled ? (slots.themeSwitch ?? InlineThemeSwitch) : undefined,
|
||||
languageSelect: i18n
|
||||
? (slots.languageSelect ?? {
|
||||
root: LanguageSelect,
|
||||
text: LanguageSelectText,
|
||||
})
|
||||
: undefined,
|
||||
searchTrigger: searchToggleEnabled
|
||||
? (slots.searchTrigger ?? {
|
||||
sm: InlineSearchTrigger,
|
||||
full: InlineSearchTriggerFull,
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
baseProps: {
|
||||
nav,
|
||||
searchToggle,
|
||||
themeSwitch,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,23 +1,10 @@
|
||||
import { useMemo, type ComponentProps, type ReactNode, type FC } from 'react';
|
||||
import { useMemo, type ComponentProps, type ReactNode } from 'react';
|
||||
import type { I18nConfig } from 'fumadocs-core/i18n';
|
||||
import type { LinkItemType } from '@/utils/link-item';
|
||||
import {
|
||||
FullSearchTrigger,
|
||||
SearchTrigger,
|
||||
type FullSearchTriggeProps,
|
||||
type SearchTriggerProps,
|
||||
} from '../slots/search-toggle';
|
||||
import { ThemeSwitch, type ThemeSwitchProps } from '../slots/theme-toggle';
|
||||
import {
|
||||
LanguageSelect,
|
||||
LanguageSelectText,
|
||||
type LanguageSelectProps,
|
||||
type LanguageSelectTextProps,
|
||||
} from '../slots/language-toggle';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import { NavTitle } from '../slots/nav-title';
|
||||
import * as PageTree from 'fumadocs-core/page-tree';
|
||||
import { isActive, normalize } from '@/utils/urls';
|
||||
import type { BaseSlots } from './client';
|
||||
import type { ThemeSwitchProps } from './slots/theme-switch';
|
||||
import type { FullSearchTriggeProps, SearchTriggerProps } from './slots/search-trigger';
|
||||
|
||||
export interface NavOptions {
|
||||
enabled?: boolean;
|
||||
@@ -55,22 +42,8 @@ export interface BaseLayoutProps {
|
||||
nav?: NavOptions;
|
||||
slots?: BaseSlots;
|
||||
children?: ReactNode;
|
||||
themeSwitch?: {
|
||||
enabled?: boolean;
|
||||
mode?: 'light-dark' | 'light-dark-system';
|
||||
|
||||
/** @deprecated use `slots.themeSwitch` instead */
|
||||
component?: ReactNode;
|
||||
};
|
||||
|
||||
searchToggle?: {
|
||||
enabled?: boolean;
|
||||
/** @deprecated use `slots.searchTrigger` instead */
|
||||
components?: {
|
||||
sm?: ReactNode;
|
||||
lg?: ReactNode;
|
||||
};
|
||||
};
|
||||
themeSwitch?: ThemeSwitchOptions;
|
||||
searchToggle?: SearchToggleOptions;
|
||||
|
||||
/**
|
||||
* @deprecated this is now optional for i18n setups, you can still customise language switch from `slots`.
|
||||
@@ -78,95 +51,21 @@ export interface BaseLayoutProps {
|
||||
i18n?: boolean | I18nConfig;
|
||||
}
|
||||
|
||||
export interface BaseSlots {
|
||||
navTitle?: FC<ComponentProps<'a'>>;
|
||||
searchTrigger?: {
|
||||
sm: FC<SearchTriggerProps>;
|
||||
full: FC<FullSearchTriggeProps>;
|
||||
interface SearchToggleOptions {
|
||||
enabled?: boolean;
|
||||
sm?: SearchTriggerProps;
|
||||
full?: FullSearchTriggeProps;
|
||||
/** @deprecated use `slots.searchTrigger` instead */
|
||||
components?: {
|
||||
sm?: ReactNode;
|
||||
lg?: ReactNode;
|
||||
};
|
||||
languageSelect?: {
|
||||
root: FC<LanguageSelectProps>;
|
||||
text: FC<LanguageSelectTextProps>;
|
||||
};
|
||||
themeSwitch?: FC<ThemeSwitchProps>;
|
||||
}
|
||||
|
||||
export type BaseSlotsProps<P extends BaseLayoutProps = BaseLayoutProps> = Pick<
|
||||
P,
|
||||
'themeSwitch' | 'searchToggle' | 'nav'
|
||||
>;
|
||||
|
||||
export function baseSlots({ useProps }: { useProps: () => BaseSlotsProps }) {
|
||||
function InlineThemeSwitch(props: ThemeSwitchProps) {
|
||||
const { mode = props.mode, component } = useProps().themeSwitch ?? {};
|
||||
if (component) return component;
|
||||
return <ThemeSwitch {...props} mode={mode} />;
|
||||
}
|
||||
|
||||
function InlineSearchTrigger(props: SearchTriggerProps) {
|
||||
const { components } = useProps().searchToggle ?? {};
|
||||
if (components?.sm) return components.sm;
|
||||
return <SearchTrigger {...props} />;
|
||||
}
|
||||
|
||||
function InlineSearchTriggerFull(props: FullSearchTriggeProps) {
|
||||
const { components } = useProps().searchToggle ?? {};
|
||||
if (components?.lg) return components.lg;
|
||||
return <FullSearchTrigger {...props} />;
|
||||
}
|
||||
|
||||
function InlineNavTitle(props: ComponentProps<'a'>) {
|
||||
const { url = props.href ?? '/', title } = useProps().nav ?? {};
|
||||
|
||||
if (typeof title === 'function') return title(props);
|
||||
return (
|
||||
<NavTitle {...props} href={url}>
|
||||
{title}
|
||||
</NavTitle>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
useProvider(options: BaseLayoutProps): {
|
||||
baseSlots: BaseSlots;
|
||||
baseProps: BaseSlotsProps;
|
||||
} {
|
||||
const { locales = [] } = useI18n();
|
||||
const {
|
||||
nav,
|
||||
slots = {},
|
||||
i18n = locales.length > 1,
|
||||
searchToggle = {},
|
||||
themeSwitch = {},
|
||||
} = options;
|
||||
|
||||
return {
|
||||
baseSlots: {
|
||||
navTitle: slots.navTitle ?? InlineNavTitle,
|
||||
themeSwitch:
|
||||
themeSwitch.enabled !== false ? (slots.themeSwitch ?? InlineThemeSwitch) : undefined,
|
||||
languageSelect: i18n
|
||||
? (slots.languageSelect ?? {
|
||||
root: LanguageSelect,
|
||||
text: LanguageSelectText,
|
||||
})
|
||||
: undefined,
|
||||
searchTrigger:
|
||||
searchToggle.enabled !== false
|
||||
? (slots.searchTrigger ?? {
|
||||
sm: InlineSearchTrigger,
|
||||
full: InlineSearchTriggerFull,
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
baseProps: {
|
||||
nav,
|
||||
searchToggle,
|
||||
themeSwitch,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
interface ThemeSwitchOptions extends ThemeSwitchProps {
|
||||
enabled?: boolean;
|
||||
/** @deprecated use `slots.themeSwitch` instead */
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
export interface LayoutTab {
|
||||
@@ -244,6 +143,113 @@ export function getLayoutTabs(
|
||||
return results;
|
||||
}
|
||||
|
||||
export function isLayoutTabActive(tab: LayoutTab, pathname: string) {
|
||||
if (tab.$folder) {
|
||||
return (
|
||||
PageTree.findPath(
|
||||
tab.$folder.children,
|
||||
(node) => node.type === 'page' && isActive(node.url, pathname),
|
||||
) !== null
|
||||
);
|
||||
}
|
||||
|
||||
if (tab.urls) {
|
||||
return tab.urls.has(normalize(pathname));
|
||||
}
|
||||
|
||||
return isActive(tab.url, pathname, true);
|
||||
}
|
||||
|
||||
interface Filterable {
|
||||
/**
|
||||
* Restrict where the item is displayed
|
||||
*
|
||||
* @defaultValue 'all'
|
||||
*/
|
||||
on?: 'menu' | 'nav' | 'all';
|
||||
}
|
||||
|
||||
interface WithHref {
|
||||
url: string;
|
||||
/**
|
||||
* When the item is marked as active
|
||||
*
|
||||
* @defaultValue 'url'
|
||||
*/
|
||||
active?: 'url' | 'nested-url' | 'none';
|
||||
external?: boolean;
|
||||
}
|
||||
|
||||
export interface MainItemType extends WithHref, Filterable {
|
||||
type?: 'main';
|
||||
icon?: ReactNode;
|
||||
text: ReactNode;
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
export interface IconItemType extends WithHref, Filterable {
|
||||
type: 'icon';
|
||||
/**
|
||||
* `aria-label` of icon button
|
||||
*/
|
||||
label?: string;
|
||||
icon: ReactNode;
|
||||
text: ReactNode;
|
||||
/**
|
||||
* @defaultValue true
|
||||
*/
|
||||
secondary?: boolean;
|
||||
}
|
||||
|
||||
export interface ButtonItemType extends WithHref, Filterable {
|
||||
type: 'button';
|
||||
icon?: ReactNode;
|
||||
text: ReactNode;
|
||||
/**
|
||||
* @defaultValue false
|
||||
*/
|
||||
secondary?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuItemType extends Partial<WithHref>, Filterable {
|
||||
type: 'menu';
|
||||
icon?: ReactNode;
|
||||
text: ReactNode;
|
||||
|
||||
items: (
|
||||
| (MainItemType & {
|
||||
/**
|
||||
* Options when displayed on navigation menu
|
||||
*/
|
||||
menu?: ComponentProps<'a'> & {
|
||||
banner?: ReactNode;
|
||||
};
|
||||
})
|
||||
| CustomItemType
|
||||
)[];
|
||||
|
||||
/**
|
||||
* @defaultValue false
|
||||
*/
|
||||
secondary?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomItemType extends Filterable {
|
||||
type: 'custom';
|
||||
/**
|
||||
* @defaultValue false
|
||||
*/
|
||||
secondary?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export type LinkItemType =
|
||||
| MainItemType
|
||||
| IconItemType
|
||||
| ButtonItemType
|
||||
| MenuItemType
|
||||
| CustomItemType;
|
||||
|
||||
/**
|
||||
* Get link items with shortcuts
|
||||
*/
|
||||
@@ -294,21 +300,11 @@ export function useLinkItems({ githubUrl, links }: Pick<BaseLayoutProps, 'links'
|
||||
}, [links, githubUrl]);
|
||||
}
|
||||
|
||||
export function isLayoutTabActive(tab: LayoutTab, pathname: string) {
|
||||
if (tab.$folder) {
|
||||
return (
|
||||
PageTree.findPath(
|
||||
tab.$folder.children,
|
||||
(node) => node.type === 'page' && isActive(node.url, pathname),
|
||||
) !== null
|
||||
);
|
||||
}
|
||||
export function isLinkItemActive(link: LinkItemType, pathname: string) {
|
||||
if (link.type === 'custom' || !link.url) return false;
|
||||
if (link.active === 'none') return false;
|
||||
|
||||
if (tab.urls) {
|
||||
return tab.urls.has(normalize(pathname));
|
||||
}
|
||||
|
||||
return isActive(tab.url, pathname, true);
|
||||
return isActive(link.url, pathname, link.active === 'nested-url');
|
||||
}
|
||||
|
||||
export type * from '@/utils/link-item';
|
||||
export { type BaseSlots, type BaseSlotsProps, baseSlots, LinkItem } from './client';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { Airplay, Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { ComponentProps, useEffect, useState } from 'react';
|
||||
import { type ComponentProps, useEffect, useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
const itemVariants = cva('size-6.5 p-1.5 text-fd-muted-foreground', {
|
||||
@@ -1,7 +0,0 @@
|
||||
'use client';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
export function NavTitle({ href, ...props }: ComponentProps<'a'>) {
|
||||
return <Link href={href ?? ''} {...props} />;
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
'use client';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { usePathname } from 'fumadocs-core/framework';
|
||||
import { isActive } from './urls';
|
||||
import Link from 'fumadocs-core/link';
|
||||
|
||||
interface Filterable {
|
||||
/**
|
||||
* Restrict where the item is displayed
|
||||
*
|
||||
* @defaultValue 'all'
|
||||
*/
|
||||
on?: 'menu' | 'nav' | 'all';
|
||||
}
|
||||
|
||||
interface WithHref {
|
||||
url: string;
|
||||
/**
|
||||
* When the item is marked as active
|
||||
*
|
||||
* @defaultValue 'url'
|
||||
*/
|
||||
active?: 'url' | 'nested-url' | 'none';
|
||||
external?: boolean;
|
||||
}
|
||||
|
||||
export interface MainItemType extends WithHref, Filterable {
|
||||
type?: 'main';
|
||||
icon?: ReactNode;
|
||||
text: ReactNode;
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
export interface IconItemType extends WithHref, Filterable {
|
||||
type: 'icon';
|
||||
/**
|
||||
* `aria-label` of icon button
|
||||
*/
|
||||
label?: string;
|
||||
icon: ReactNode;
|
||||
text: ReactNode;
|
||||
/**
|
||||
* @defaultValue true
|
||||
*/
|
||||
secondary?: boolean;
|
||||
}
|
||||
|
||||
export interface ButtonItemType extends WithHref, Filterable {
|
||||
type: 'button';
|
||||
icon?: ReactNode;
|
||||
text: ReactNode;
|
||||
/**
|
||||
* @defaultValue false
|
||||
*/
|
||||
secondary?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuItemType extends Partial<WithHref>, Filterable {
|
||||
type: 'menu';
|
||||
icon?: ReactNode;
|
||||
text: ReactNode;
|
||||
|
||||
items: (
|
||||
| (MainItemType & {
|
||||
/**
|
||||
* Options when displayed on navigation menu
|
||||
*/
|
||||
menu?: ComponentProps<'a'> & {
|
||||
banner?: ReactNode;
|
||||
};
|
||||
})
|
||||
| CustomItemType
|
||||
)[];
|
||||
|
||||
/**
|
||||
* @defaultValue false
|
||||
*/
|
||||
secondary?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomItemType extends Filterable {
|
||||
type: 'custom';
|
||||
/**
|
||||
* @defaultValue false
|
||||
*/
|
||||
secondary?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export type LinkItemType =
|
||||
| MainItemType
|
||||
| IconItemType
|
||||
| ButtonItemType
|
||||
| MenuItemType
|
||||
| CustomItemType;
|
||||
|
||||
export function LinkItem({
|
||||
ref,
|
||||
item,
|
||||
...props
|
||||
}: Omit<ComponentProps<'a'>, 'href'> & { item: LinkItemType & WithHref }) {
|
||||
const active = useLinkItemActive(item);
|
||||
|
||||
return (
|
||||
<Link ref={ref} href={item.url} external={item.external} {...props} data-active={active}>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLinkItemActive(link: LinkItemType) {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (link.type === 'custom' || !link.url) return false;
|
||||
if (link.active === 'none') return false;
|
||||
|
||||
return isActive(link.url, pathname, link.active === 'nested-url');
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { createElement, type ElementType, type FC, type ReactNode } from 'react';
|
||||
|
||||
export type Renderer<Props> = FC<Props> | Partial<Props> | ChildrenRenderer | boolean;
|
||||
|
||||
export type RenderFn<Props> = (props: (override?: Partial<Props>) => Props) => ReactNode;
|
||||
|
||||
export function renderer<Props extends object>(
|
||||
T: Renderer<Props>,
|
||||
Default: FC<Props> | ElementType<Props>,
|
||||
): RenderFn<Props> | undefined {
|
||||
if (T === false) return;
|
||||
if (T === true)
|
||||
return function Wrapper(props) {
|
||||
return createElement(Default, props());
|
||||
};
|
||||
if (T instanceof ChildrenRenderer)
|
||||
return function Wrapper() {
|
||||
return T.children;
|
||||
};
|
||||
if (typeof T === 'object')
|
||||
return function Wrapper(props) {
|
||||
return createElement(Default, props(T));
|
||||
};
|
||||
return function Wrapper(props) {
|
||||
return createElement(T, props());
|
||||
};
|
||||
}
|
||||
|
||||
export class ChildrenRenderer {
|
||||
constructor(readonly children: ReactNode) {}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ export default defineConfig({
|
||||
entry: [
|
||||
'./src/*.{ts,tsx}',
|
||||
'./src/{components,contexts,provider,tailwind,og}/**/*.{ts,tsx}',
|
||||
'./src/layouts/*/{index,client}.tsx',
|
||||
'./src/layouts/*/page/{index,client}.tsx',
|
||||
'./src/layouts/*/index.tsx',
|
||||
'./src/layouts/*/page/index.tsx',
|
||||
'./src/layouts/**/slots/*',
|
||||
'./src/layouts/home/{navbar,not-found}.tsx',
|
||||
'./src/utils/use-*.{ts,tsx}',
|
||||
'./src/utils/renderer.ts',
|
||||
],
|
||||
fixedExtension: false,
|
||||
unbundle: true,
|
||||
@@ -32,4 +32,25 @@ export default defineConfig({
|
||||
deps: {
|
||||
onlyBundle: [],
|
||||
},
|
||||
exports: {
|
||||
exclude: ['mdx.server', 'tailwind/typography'],
|
||||
customExports: {
|
||||
'./style.css': './dist/style.css',
|
||||
'./css/*': './css/*',
|
||||
'./image-zoom.css': './dist/image-zoom.css',
|
||||
'./utils/get-sidebar-tabs': {
|
||||
types: './dist/components/sidebar/tabs/index.d.ts',
|
||||
import: './dist/components/sidebar/tabs/index.js',
|
||||
},
|
||||
'./components/tabs.unstyled': {
|
||||
types: './dist/components/ui/tabs.d.ts',
|
||||
import: './dist/components/ui/tabs.js',
|
||||
},
|
||||
'./mdx': {
|
||||
types: './dist/mdx.d.ts',
|
||||
node: './dist/mdx.server.js',
|
||||
import: './dist/mdx.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,7 +49,7 @@ export function resolveExternal(
|
||||
if (ref.type === 'file') {
|
||||
const file = path.relative(toRegistryDir, ref.file).replaceAll(path.sep, '/');
|
||||
|
||||
if (file === 'utils/renderer.ts' || /^contexts\//.test(file) || /^utils\/use-/.test(file)) {
|
||||
if (/^contexts\//.test(file) || /^utils\/use-/.test(file)) {
|
||||
return {
|
||||
dep: toPackageName,
|
||||
type: 'dependency',
|
||||
@@ -62,9 +62,9 @@ export function resolveExternal(
|
||||
export async function findSlotComponents(dir: string): Promise<Component[]> {
|
||||
const slots: Component[] = [];
|
||||
|
||||
for await (const file of glob('**/slots/*.tsx', { cwd: dir })) {
|
||||
for await (const file of glob('layouts/**/slots/*', { cwd: dir })) {
|
||||
const name = path.relative('layouts', file);
|
||||
if (name.startsWith('..')) continue;
|
||||
|
||||
slots.push({
|
||||
name: name.slice(0, -path.extname(name).length),
|
||||
unlisted: true,
|
||||
@@ -112,17 +112,6 @@ export const commonComponents: Component[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'link-item',
|
||||
unlisted: true,
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
path: 'utils/link-item.tsx',
|
||||
target: '<dir>/layout/link-item.tsx',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'toc',
|
||||
unlisted: true,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": "tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"target": "esnext"
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "eslint.config.mjs"],
|
||||
"exclude": ["node_modules", "dist", "eslint.config.mjs"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user