UI: support on on docs layout

This commit is contained in:
Fuma Nama
2026-01-29 19:30:40 +08:00
parent 6fd7e635a9
commit 430a5f1bd5
9 changed files with 83 additions and 77 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'fumadocs-ui': patch
'@fumadocs/base-ui': patch
---
support `on` on docs layout
+4 -4
View File
@@ -13,7 +13,7 @@ import {
SidebarTrigger,
SidebarViewport,
} from './sidebar';
import { type BaseLayoutProps, renderTitleNav, resolveLinkItems } from '@/layouts/shared';
import { type BaseLayoutProps, renderTitleNav, useLinkItems } from '@/layouts/shared';
import { LinkItem } from '@fumadocs/ui/link-item';
import { LanguageToggle, LanguageToggleText } from '@/layouts/shared/language-toggle';
import { LayoutBody, LayoutContextProvider, LayoutHeader, LayoutTabs } from './client';
@@ -90,16 +90,16 @@ export function DocsLayout({
}
return [];
}, [tree, sidebarTabs]);
const links = resolveLinkItems(props);
const { menuItems } = useLinkItems(props);
function sidebar() {
const { footer, banner, collapsible = true, component, components, ...rest } = sidebarProps;
if (component) return component;
const iconLinks = links.filter((item) => item.type === 'icon');
const iconLinks = menuItems.filter((item) => item.type === 'icon');
const viewport = (
<SidebarViewport>
{links
{menuItems
.filter((v) => v.type !== 'icon')
.map((item, i, list) => (
<SidebarLinkItem key={i} item={item} className={cn(i === list.length - 1 && 'mb-4')} />
+6 -30
View File
@@ -12,12 +12,7 @@ import {
import { cva } from 'class-variance-authority';
import Link from 'fumadocs-core/link';
import { cn } from '@fumadocs/ui/cn';
import {
type LinkItemType,
type NavOptions,
renderTitleNav,
resolveLinkItems,
} from '@/layouts/shared';
import { type LinkItemType, type NavOptions, renderTitleNav, useLinkItems } from '@/layouts/shared';
import { LinkItem } from '@fumadocs/ui/link-item';
import {
NavigationMenuRoot,
@@ -45,7 +40,7 @@ const MobileMenuContext = createContext<{
export const navItemVariants = cva('[&_svg]:size-4', {
variants: {
variant: {
main: 'inline-flex items-center gap-1 p-2 text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground data-[active=true]:text-fd-primary data-[popup-open]:text-fd-primary',
main: 'inline-flex items-center gap-1 p-2 text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground data-[active=true]:text-fd-primary data-popup-open:text-fd-primary',
button: buttonVariants({
color: 'secondary',
className: 'gap-1.5',
@@ -69,26 +64,7 @@ export function Header({
themeSwitch = {},
searchToggle = {},
}: HomeLayoutProps) {
const { navItems, menuItems } = useMemo(() => {
const navItems: LinkItemType[] = [];
const menuItems: LinkItemType[] = [];
for (const item of resolveLinkItems({ links, githubUrl })) {
switch (item.on ?? 'all') {
case 'menu':
menuItems.push(item);
break;
case 'nav':
navItems.push(item);
break;
default:
navItems.push(item);
menuItems.push(item);
}
}
return { navItems, menuItems };
}, [links, githubUrl]);
const { menuItems, navItems } = useLinkItems({ links, githubUrl });
return (
<MobileMenuCollapsible
@@ -147,7 +123,7 @@ export function Header({
}),
)}
>
<ChevronDown className="transition-transform duration-300 group-data-[panel-open]:rotate-180" />
<ChevronDown className="transition-transform duration-300 group-data-panel-open:rotate-180" />
</CollapsibleTrigger>
</ul>
</NavigationMenuList>
@@ -180,13 +156,13 @@ export function Header({
<NavigationMenu.Portal>
<NavigationMenu.Positioner
sideOffset={10}
className="z-20 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[left,right] duration-(--duration) ease-(--easing) data-[instant]:transition-none"
className="z-20 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[left,right] duration-(--duration) ease-(--easing) data-instant:transition-none"
style={{
['--duration' as string]: '0.35s',
['--easing' as string]: 'cubic-bezier(0.22, 1, 0.36, 1)',
}}
>
<NavigationMenu.Popup className="relative w-(--popup-width) h-(--popup-height) max-w-(--fd-layout-width,1400px) origin-(--transform-origin) rounded-xl bg-fd-background/80 border backdrop-blur-lg shadow-lg transition-[opacity,transform,width,height,scale,translate] duration-(--duration) ease-(--easing) data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[ending-style]:duration-150 data-[starting-style]:scale-90 data-[starting-style]:opacity-0">
<NavigationMenu.Popup className="relative w-(--popup-width) h-(--popup-height) max-w-(--fd-layout-width,1400px) origin-(--transform-origin) rounded-xl bg-fd-background/80 border backdrop-blur-lg shadow-lg transition-[opacity,transform,width,height,scale,translate] duration-(--duration) ease-(--easing) data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0">
<NavigationMenu.Viewport className="relative size-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
@@ -1,5 +1,5 @@
import { type ComponentProps, type FC, type HTMLAttributes, type ReactNode, useMemo } from 'react';
import { type BaseLayoutProps, renderTitleNav, resolveLinkItems } from '@/layouts/shared';
import { type BaseLayoutProps, renderTitleNav, useLinkItems } from '@/layouts/shared';
import {
Sidebar,
SidebarCollapseTrigger,
@@ -76,7 +76,7 @@ export function DocsLayout(props: DocsLayoutProps) {
} = props;
const navMode = nav.mode ?? 'auto';
const links = resolveLinkItems(props);
const { menuItems, navItems } = useLinkItems(props);
const tabs = useMemo(() => {
if (Array.isArray(tabOptions)) {
return tabOptions;
@@ -96,7 +96,7 @@ export function DocsLayout(props: DocsLayoutProps) {
function sidebar() {
const { banner, footer, components, collapsible = true, ...rest } = sidebarProps;
const iconLinks = links.filter((item) => item.type === 'icon');
const iconLinks = menuItems.filter((item) => item.type === 'icon');
const Header =
typeof banner === 'function'
? banner
@@ -124,7 +124,7 @@ export function DocsLayout(props: DocsLayoutProps) {
);
const viewport = (
<SidebarViewport>
{links
{menuItems
.filter((item) => item.type !== 'icon')
.map((item, i, arr) => (
<SidebarLinkItem
@@ -254,7 +254,7 @@ export function DocsLayout(props: DocsLayoutProps) {
<Sidebar defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
<LayoutBody {...props.containerProps}>
{sidebar()}
<DocsNavbar {...props} links={links} tabs={tabs} />
<DocsNavbar {...props} links={navItems} tabs={tabs} />
{props.children}
</LayoutBody>
</Sidebar>
+25 -1
View File
@@ -1,4 +1,4 @@
import type { ComponentProps, ReactNode } from 'react';
import { useMemo, type ComponentProps, type ReactNode } from 'react';
import type { I18nConfig } from 'fumadocs-core/i18n';
import type { LinkItemType } from '@fumadocs/ui/link-item';
import Link from 'fumadocs-core/link';
@@ -99,4 +99,28 @@ export function renderTitleNav(
);
}
export function useLinkItems({ githubUrl, links }: Pick<BaseLayoutProps, 'links' | 'githubUrl'>) {
return useMemo(() => {
const all = resolveLinkItems({ links, githubUrl });
const navItems: LinkItemType[] = [];
const menuItems: LinkItemType[] = [];
for (const item of all) {
switch (item.on ?? 'all') {
case 'menu':
menuItems.push(item);
break;
case 'nav':
navItems.push(item);
break;
default:
navItems.push(item);
menuItems.push(item);
}
}
return { navItems, menuItems, all };
}, [links, githubUrl]);
}
export type * from '@fumadocs/ui/link-item';
+4 -4
View File
@@ -13,7 +13,7 @@ import {
SidebarTrigger,
SidebarViewport,
} from './sidebar';
import { type BaseLayoutProps, renderTitleNav, resolveLinkItems } from '@/layouts/shared';
import { type BaseLayoutProps, renderTitleNav, useLinkItems } from '@/layouts/shared';
import { LinkItem } from '@fumadocs/ui/link-item';
import { LanguageToggle, LanguageToggleText } from '@/layouts/shared/language-toggle';
import { LayoutBody, LayoutContextProvider, LayoutHeader, LayoutTabs } from './client';
@@ -90,16 +90,16 @@ export function DocsLayout({
}
return [];
}, [tree, sidebarTabs]);
const links = resolveLinkItems(props);
const { menuItems } = useLinkItems(props);
function sidebar() {
const { footer, banner, collapsible = true, component, components, ...rest } = sidebarProps;
if (component) return component;
const iconLinks = links.filter((item) => item.type === 'icon');
const iconLinks = menuItems.filter((item) => item.type === 'icon');
const viewport = (
<SidebarViewport>
{links
{menuItems
.filter((v) => v.type !== 'icon')
.map((item, i, list) => (
<SidebarLinkItem key={i} item={item} className={cn(i === list.length - 1 && 'mb-4')} />
+3 -27
View File
@@ -1,14 +1,9 @@
'use client';
import { type ComponentProps, Fragment, useMemo, useState } from 'react';
import { type ComponentProps, Fragment, useState } from 'react';
import { cva } from 'class-variance-authority';
import Link from 'fumadocs-core/link';
import { cn } from '@fumadocs/ui/cn';
import {
type LinkItemType,
type NavOptions,
renderTitleNav,
resolveLinkItems,
} from '@/layouts/shared';
import { type LinkItemType, type NavOptions, renderTitleNav, useLinkItems } from '@/layouts/shared';
import { LinkItem } from '@fumadocs/ui/link-item';
import {
NavigationMenu,
@@ -54,26 +49,7 @@ export function Header({
themeSwitch = {},
searchToggle = {},
}: HomeLayoutProps) {
const { navItems, menuItems } = useMemo(() => {
const navItems: LinkItemType[] = [];
const menuItems: LinkItemType[] = [];
for (const item of resolveLinkItems({ links, githubUrl })) {
switch (item.on ?? 'all') {
case 'menu':
menuItems.push(item);
break;
case 'nav':
navItems.push(item);
break;
default:
navItems.push(item);
menuItems.push(item);
}
}
return { navItems, menuItems };
}, [links, githubUrl]);
const { navItems, menuItems } = useLinkItems({ links, githubUrl });
return (
<HeaderNavigationMenu transparentMode={nav.transparentMode}>
@@ -1,5 +1,5 @@
import { type ComponentProps, type FC, type HTMLAttributes, type ReactNode, useMemo } from 'react';
import { type BaseLayoutProps, renderTitleNav, resolveLinkItems } from '@/layouts/shared';
import { type BaseLayoutProps, renderTitleNav, useLinkItems } from '@/layouts/shared';
import {
Sidebar,
SidebarCollapseTrigger,
@@ -76,7 +76,7 @@ export function DocsLayout(props: DocsLayoutProps) {
} = props;
const navMode = nav.mode ?? 'auto';
const links = resolveLinkItems(props);
const { menuItems, navItems } = useLinkItems(props);
const tabs = useMemo(() => {
if (Array.isArray(tabOptions)) {
return tabOptions;
@@ -96,7 +96,7 @@ export function DocsLayout(props: DocsLayoutProps) {
function sidebar() {
const { banner, footer, components, collapsible = true, ...rest } = sidebarProps;
const iconLinks = links.filter((item) => item.type === 'icon');
const iconLinks = menuItems.filter((item) => item.type === 'icon');
const Header =
typeof banner === 'function'
? banner
@@ -124,7 +124,7 @@ export function DocsLayout(props: DocsLayoutProps) {
);
const viewport = (
<SidebarViewport>
{links
{menuItems
.filter((item) => item.type !== 'icon')
.map((item, i, arr) => (
<SidebarLinkItem
@@ -254,7 +254,7 @@ export function DocsLayout(props: DocsLayoutProps) {
<Sidebar defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
<LayoutBody {...props.containerProps}>
{sidebar()}
<DocsNavbar {...props} links={links} tabs={tabs} />
<DocsNavbar {...props} links={navItems} tabs={tabs} />
{props.children}
</LayoutBody>
</Sidebar>
+25 -1
View File
@@ -1,4 +1,4 @@
import type { ComponentProps, ReactNode } from 'react';
import { useMemo, type ComponentProps, type ReactNode } from 'react';
import type { I18nConfig } from 'fumadocs-core/i18n';
import type { LinkItemType } from '@fumadocs/ui/link-item';
import Link from 'fumadocs-core/link';
@@ -99,4 +99,28 @@ export function renderTitleNav(
);
}
export function useLinkItems({ githubUrl, links }: Pick<BaseLayoutProps, 'links' | 'githubUrl'>) {
return useMemo(() => {
const all = resolveLinkItems({ links, githubUrl });
const navItems: LinkItemType[] = [];
const menuItems: LinkItemType[] = [];
for (const item of all) {
switch (item.on) {
case 'menu':
menuItems.push(item);
break;
case 'nav':
navItems.push(item);
break;
default:
navItems.push(item);
menuItems.push(item);
}
}
return { navItems, menuItems, all };
}, [links, githubUrl]);
}
export type * from '@fumadocs/ui/link-item';