UI: improve navbar button gaps

This commit is contained in:
Fuma Nama
2025-11-07 19:12:34 +08:00
parent 71b7e2079c
commit c0df2c4c38
8 changed files with 284 additions and 294 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-ui': patch
---
improve navbar button gaps
+1 -1
View File
@@ -12,7 +12,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
{...base}
tree={source.pageTree}
// just icon items
links={linkItems.filter((item) => item.type === 'icon')}
links={[...linkItems.filter((item) => item.type === 'icon')]}
nav={{
...base.nav,
title: (
+3 -3
View File
@@ -43,11 +43,11 @@ export const logo = (
alt="Fumadocs"
src={Logo}
sizes="100px"
className="hidden w-22 [.uwu_&]:block"
className="hidden w-22 in-[.uwu]:block"
aria-label="Fumadocs"
/>
<FumadocsIcon className="size-5 [.uwu_&]:hidden" />
<FumadocsIcon className="size-5 in-[.uwu]:hidden" />
</>
);
@@ -57,7 +57,7 @@ export function baseOptions(): BaseLayoutProps {
title: (
<>
{logo}
<span className="font-medium [.uwu_&]:hidden [header_&]:text-[15px]">
<span className="font-medium in-[.uwu]:hidden in-[header]:text-[15px]">
Fumadocs
</span>
</>
+3 -3
View File
@@ -28,9 +28,9 @@
"import": "./dist/layouts/home/index.js",
"types": "./dist/layouts/home/index.d.ts"
},
"./layouts/home/*": {
"import": "./dist/layouts/home/*.js",
"types": "./dist/layouts/home/*.d.ts"
"./layouts/home/navbar": {
"import": "./dist/layouts/home/navbar.js",
"types": "./dist/layouts/home/navbar.d.ts"
},
"./layouts/docs": {
"import": "./dist/layouts/docs/index.js",
+253
View File
@@ -0,0 +1,253 @@
'use client';
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 { BaseLinkItem, LinkItemType } from '@/layouts/shared';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
NavigationMenuViewport,
} from '@/components/ui/navigation-menu';
import { useNav } from '@/contexts/layout';
import { buttonVariants } from '@/components/ui/button';
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',
button: buttonVariants({
color: 'secondary',
className: 'gap-1.5',
}),
icon: buttonVariants({
color: 'ghost',
size: 'icon',
}),
},
},
defaultVariants: {
variant: 'main',
},
});
export function Navbar(props: ComponentProps<'div'>) {
const [value, setValue] = useState('');
const { isTransparent } = useNav();
return (
<NavigationMenu value={value} onValueChange={setValue} asChild>
<header
id="nd-nav"
{...props}
className={cn(
'fixed top-(--fd-banner-height) z-40 left-0 right-(--removed-body-scroll-bar-size,0) backdrop-blur-lg border-b transition-colors *:mx-auto *:max-w-fd-container',
value.length > 0 && 'max-lg:shadow-lg max-lg:rounded-b-2xl',
(!isTransparent || value.length > 0) && 'bg-fd-background/80',
props.className,
)}
>
<NavigationMenuList
className="flex h-14 w-full items-center px-4"
asChild
>
<nav>{props.children}</nav>
</NavigationMenuList>
<NavigationMenuViewport />
</header>
</NavigationMenu>
);
}
export function NavbarLinkItem({
item,
...props
}: {
item: LinkItemType;
className?: string;
}) {
if (item.type === 'custom') return <div {...props}>{item.children}</div>;
if (item.type === 'menu') {
const children = item.items.map((child, j) => {
if (child.type === 'custom') {
return <Fragment key={j}>{child.children}</Fragment>;
}
const {
banner = child.icon ? (
<div className="w-fit rounded-md border bg-fd-muted p-1 [&_svg]:size-4">
{child.icon}
</div>
) : null,
...rest
} = child.menu ?? {};
return (
<NavigationMenuLink key={`${j}-${child.url}`} asChild>
<Link
href={child.url}
external={child.external}
{...rest}
className={cn(
'flex flex-col gap-2 rounded-lg border bg-fd-card p-3 transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground',
rest.className,
)}
>
{rest.children ?? (
<>
{banner}
<p className="text-[15px] font-medium">{child.text}</p>
<p className="text-sm text-fd-muted-foreground empty:hidden">
{child.description}
</p>
</>
)}
</Link>
</NavigationMenuLink>
);
});
return (
<NavigationMenuItem>
<NavigationMenuTrigger
{...props}
className={cn(navItemVariants(), 'rounded-md', props.className)}
>
{item.url ? (
<Link href={item.url} external={item.external}>
{item.text}
</Link>
) : (
item.text
)}
</NavigationMenuTrigger>
<NavigationMenuContent className="grid grid-cols-1 gap-2 p-4 md:grid-cols-2 lg:grid-cols-3">
{children}
</NavigationMenuContent>
</NavigationMenuItem>
);
}
return (
<NavigationMenuItem>
<NavigationMenuLink asChild>
<BaseLinkItem
item={item}
aria-label={item.type === 'icon' ? item.label : undefined}
{...props}
className={cn(
navItemVariants({ variant: item.type }),
props.className,
)}
>
{item.type === 'icon' ? item.icon : item.text}
</BaseLinkItem>
</NavigationMenuLink>
</NavigationMenuItem>
);
}
export const Menu = NavigationMenuItem;
export function MenuLinkItem({
item,
...props
}: {
item: LinkItemType;
className?: string;
}) {
if (item.type === 'custom')
return <div className={cn('grid', props.className)}>{item.children}</div>;
if (item.type === 'menu') {
const header = (
<>
{item.icon}
{item.text}
</>
);
return (
<div className={cn('mb-4 flex flex-col', props.className)}>
<p className="mb-1 text-sm text-fd-muted-foreground">
{item.url ? (
<NavigationMenuLink asChild>
<Link href={item.url} external={item.external}>
{header}
</Link>
</NavigationMenuLink>
) : (
header
)}
</p>
{item.items.map((child, i) => (
<MenuLinkItem key={i} item={child} />
))}
</div>
);
}
return (
<NavigationMenuLink asChild>
<BaseLinkItem
item={item}
className={cn(
{
main: 'inline-flex items-center gap-2 py-1.5 transition-colors hover:text-fd-popover-foreground/50 data-[active=true]:font-medium data-[active=true]:text-fd-primary [&_svg]:size-4',
icon: buttonVariants({
size: 'icon',
color: 'ghost',
}),
button: buttonVariants({
color: 'secondary',
className: 'gap-1.5 [&_svg]:size-4',
}),
}[item.type ?? 'main'],
props.className,
)}
aria-label={item.type === 'icon' ? item.label : undefined}
>
{item.icon}
{item.type === 'icon' ? undefined : item.text}
</BaseLinkItem>
</NavigationMenuLink>
);
}
export function MenuTrigger({
enableHover = false,
...props
}: ComponentProps<typeof NavigationMenuTrigger> & {
/**
* Enable hover to trigger
*/
enableHover?: boolean;
}) {
return (
<NavigationMenuTrigger
{...props}
onPointerMove={enableHover ? undefined : (e) => e.preventDefault()}
>
{props.children}
</NavigationMenuTrigger>
);
}
export function MenuContent(
props: ComponentProps<typeof NavigationMenuContent>,
) {
return (
<NavigationMenuContent
{...props}
className={cn('flex flex-col p-4', props.className)}
>
{props.children}
</NavigationMenuContent>
);
}
+18 -86
View File
@@ -1,4 +1,4 @@
import { Fragment, type HTMLAttributes, useMemo } from 'react';
import { type HTMLAttributes, useMemo } from 'react';
import { cn } from '@/utils/cn';
import {
type BaseLayoutProps,
@@ -7,14 +7,6 @@ import {
type NavOptions,
} from '@/layouts/shared';
import { NavProvider } from '@/contexts/layout';
import {
Navbar,
NavbarLink,
NavbarMenu,
NavbarMenuContent,
NavbarMenuLink,
NavbarMenuTrigger,
} from '@/layouts/home/navbar';
import {
LargeSearchToggle,
SearchToggle,
@@ -27,11 +19,13 @@ import {
import { ChevronDown, Languages } from 'lucide-react';
import Link from 'fumadocs-core/link';
import {
Navbar,
NavbarLinkItem,
Menu,
MenuContent,
MenuLinkItem,
MenuTrigger,
} from '@/layouts/home/menu';
} from '@/layouts/home/client';
import { buttonVariants } from '@/components/ui/button';
export interface HomeLayoutProps extends BaseLayoutProps {
@@ -128,14 +122,24 @@ export function Header({
))}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? <ThemeToggle mode={themeSwitch?.mode} />)}
{i18n ? (
{i18n && (
<LanguageToggle>
<Languages className="size-5" />
</LanguageToggle>
) : null}
<ul className="flex flex-row items-center empty:hidden">
)}
<ul className="flex flex-row gap-2 items-center empty:hidden">
{navItems.filter(isSecondary).map((item, i) => (
<NavbarLinkItem key={i} item={item} />
<NavbarLinkItem
key={i}
item={item}
className={cn(
item.type === 'icon' && [
'-mx-1',
i === 0 && 'ms-0',
i === navItems.length - 1 && 'me-0',
],
)}
/>
))}
</ul>
</div>
@@ -188,78 +192,6 @@ export function Header({
);
}
function NavbarLinkItem({
item,
...props
}: {
item: LinkItemType;
className?: string;
}) {
if (item.type === 'custom') return <div {...props}>{item.children}</div>;
if (item.type === 'menu') {
const children = item.items.map((child, j) => {
if (child.type === 'custom') {
return <Fragment key={j}>{child.children}</Fragment>;
}
const {
banner = child.icon ? (
<div className="w-fit rounded-md border bg-fd-muted p-1 [&_svg]:size-4">
{child.icon}
</div>
) : null,
...rest
} = child.menu ?? {};
return (
<NavbarMenuLink
key={`${j}-${child.url}`}
href={child.url}
external={child.external}
{...rest}
>
{rest.children ?? (
<>
{banner}
<p className="text-[15px] font-medium">{child.text}</p>
<p className="text-sm text-fd-muted-foreground empty:hidden">
{child.description}
</p>
</>
)}
</NavbarMenuLink>
);
});
return (
<NavbarMenu>
<NavbarMenuTrigger {...props}>
{item.url ? (
<Link href={item.url} external={item.external}>
{item.text}
</Link>
) : (
item.text
)}
</NavbarMenuTrigger>
<NavbarMenuContent>{children}</NavbarMenuContent>
</NavbarMenu>
);
}
return (
<NavbarLink
{...props}
item={item}
variant={item.type}
aria-label={item.type === 'icon' ? item.label : undefined}
>
{item.type === 'icon' ? item.icon : item.text}
</NavbarLink>
);
}
function isSecondary(item: LinkItemType): boolean {
if ('secondary' in item && item.secondary != null) return item.secondary;
-121
View File
@@ -1,121 +0,0 @@
'use client';
import { BaseLinkItem, type LinkItemType } from '@/layouts/shared';
import { cn } from '@/utils/cn';
import {
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuTrigger,
} from '@/components/ui/navigation-menu';
import Link from 'fumadocs-core/link';
import { cva } from 'class-variance-authority';
import { buttonVariants } from '@/components/ui/button';
import type { ComponentPropsWithoutRef } from 'react';
const menuItemVariants = cva('', {
variants: {
variant: {
main: 'inline-flex items-center gap-2 py-1.5 transition-colors hover:text-fd-popover-foreground/50 data-[active=true]:font-medium data-[active=true]:text-fd-primary [&_svg]:size-4',
icon: buttonVariants({
size: 'icon',
color: 'ghost',
}),
button: buttonVariants({
color: 'secondary',
className: 'gap-1.5 [&_svg]:size-4',
}),
},
},
defaultVariants: {
variant: 'main',
},
});
export function MenuLinkItem({
item,
...props
}: {
item: LinkItemType;
className?: string;
}) {
if (item.type === 'custom')
return <div className={cn('grid', props.className)}>{item.children}</div>;
if (item.type === 'menu') {
const header = (
<>
{item.icon}
{item.text}
</>
);
return (
<div className={cn('mb-4 flex flex-col', props.className)}>
<p className="mb-1 text-sm text-fd-muted-foreground">
{item.url ? (
<NavigationMenuLink asChild>
<Link href={item.url} external={item.external}>
{header}
</Link>
</NavigationMenuLink>
) : (
header
)}
</p>
{item.items.map((child, i) => (
<MenuLinkItem key={i} item={child} />
))}
</div>
);
}
return (
<NavigationMenuLink asChild>
<BaseLinkItem
item={item}
className={cn(
menuItemVariants({ variant: item.type }),
props.className,
)}
aria-label={item.type === 'icon' ? item.label : undefined}
>
{item.icon}
{item.type === 'icon' ? undefined : item.text}
</BaseLinkItem>
</NavigationMenuLink>
);
}
export const Menu = NavigationMenuItem;
export function MenuTrigger({
enableHover = false,
...props
}: ComponentPropsWithoutRef<typeof NavigationMenuTrigger> & {
/**
* Enable hover to trigger
*/
enableHover?: boolean;
}) {
return (
<NavigationMenuTrigger
{...props}
onPointerMove={enableHover ? undefined : (e) => e.preventDefault()}
>
{props.children}
</NavigationMenuTrigger>
);
}
export function MenuContent(
props: ComponentPropsWithoutRef<typeof NavigationMenuContent>,
) {
return (
<NavigationMenuContent
{...props}
className={cn('flex flex-col p-4', props.className)}
>
{props.children}
</NavigationMenuContent>
);
}
+1 -80
View File
@@ -1,57 +1,17 @@
'use client';
import { type ComponentProps, useState } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import Link, { type LinkProps } from 'fumadocs-core/link';
import { cn } from '@/utils/cn';
import { BaseLinkItem } from '@/layouts/shared';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
NavigationMenuViewport,
} from '@/components/ui/navigation-menu';
import { useNav } from '@/contexts/layout';
import type {
NavigationMenuContentProps,
NavigationMenuTriggerProps,
} from '@radix-ui/react-navigation-menu';
import { buttonVariants } from '@/components/ui/button';
const navItemVariants = cva(
'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 [&_svg]:size-4',
);
export function Navbar(props: ComponentProps<'div'>) {
const [value, setValue] = useState('');
const { isTransparent } = useNav();
return (
<NavigationMenu value={value} onValueChange={setValue} asChild>
<header
id="nd-nav"
{...props}
className={cn(
'fixed top-(--fd-banner-height) z-40 left-0 right-(--removed-body-scroll-bar-size,0) backdrop-blur-lg border-b transition-colors *:mx-auto *:max-w-fd-container',
value.length > 0 && 'max-lg:shadow-lg max-lg:rounded-b-2xl',
(!isTransparent || value.length > 0) && 'bg-fd-background/80',
props.className,
)}
>
<NavigationMenuList
className="flex h-14 w-full items-center px-4"
asChild
>
<nav>{props.children}</nav>
</NavigationMenuList>
<NavigationMenuViewport />
</header>
</NavigationMenu>
);
}
import { navItemVariants } from './client';
export const NavbarMenu = NavigationMenuItem;
@@ -95,42 +55,3 @@ export function NavbarMenuLink(props: LinkProps) {
</NavigationMenuLink>
);
}
const linkVariants = cva('', {
variants: {
variant: {
main: navItemVariants(),
button: buttonVariants({
color: 'secondary',
className: 'gap-1.5 [&_svg]:size-4',
}),
icon: buttonVariants({
color: 'ghost',
size: 'icon',
}),
},
},
defaultVariants: {
variant: 'main',
},
});
export function NavbarLink({
item,
variant,
...props
}: ComponentProps<typeof BaseLinkItem> & VariantProps<typeof linkVariants>) {
return (
<NavigationMenuItem>
<NavigationMenuLink asChild>
<BaseLinkItem
{...props}
item={item}
className={cn(linkVariants({ variant }), props.className)}
>
{props.children}
</BaseLinkItem>
</NavigationMenuLink>
</NavigationMenuItem>
);
}