UI: Improve sidebar UI

This commit is contained in:
Fuma Nama
2025-03-20 19:18:33 +08:00
parent 866279062d
commit 44d5acf0ce
8 changed files with 150 additions and 110 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-ui': patch
---
Improve sidebar UI
+60 -53
View File
@@ -26,72 +26,79 @@ export function remarkInclude(this: Processor): Transformer<Root, Root> {
) {
const queue: Promise<void>[] = [];
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
let specifier: string | undefined;
const params: Record<string, string | null> = {};
visit(
tree,
['mdxJsxFlowElement', 'mdxJsxTextElement'],
(node, _, parent) => {
let specifier: string | undefined;
const params: Record<string, string | null> = {};
if (
(node.type === 'mdxJsxFlowElement' ||
node.type === 'mdxJsxTextElement') &&
node.name === TagName
) {
const value = flattenNode(node);
if (
(node.type === 'mdxJsxFlowElement' ||
node.type === 'mdxJsxTextElement') &&
node.name === TagName
) {
const value = flattenNode(node);
if (value.length > 0) {
for (const attr of node.attributes) {
if (
attr.type === 'mdxJsxAttribute' &&
(typeof attr.value === 'string' || attr.value === null)
) {
params[attr.name] = attr.value;
if (value.length > 0) {
for (const attr of node.attributes) {
if (
attr.type === 'mdxJsxAttribute' &&
(typeof attr.value === 'string' || attr.value === null)
) {
params[attr.name] = attr.value;
}
}
specifier = value;
}
specifier = value;
}
}
if (!specifier) return;
if (!specifier) return;
const targetPath = path.resolve(
'cwd' in params ? process.cwd() : path.dirname(file),
specifier,
);
const asCode =
params.lang ||
(!specifier.endsWith('.md') && !specifier.endsWith('.mdx'));
const targetPath = path.resolve(
'cwd' in params ? process.cwd() : path.dirname(file),
specifier,
);
const asCode =
params.lang ||
(!specifier.endsWith('.md') && !specifier.endsWith('.mdx'));
queue.push(
fs
.readFile(targetPath)
.then(async (content) => {
compiler?.addDependency(targetPath);
queue.push(
fs
.readFile(targetPath)
.then(async (content) => {
compiler?.addDependency(targetPath);
if (asCode) {
const lang = params.lang ?? path.extname(specifier).slice(1);
if (asCode) {
const lang = params.lang ?? path.extname(specifier).slice(1);
Object.assign(node, {
type: 'code',
lang,
meta: params.meta,
value: content.toString(),
data: {},
} satisfies Code);
return;
}
Object.assign(node, {
type: 'code',
lang,
meta: params.meta,
value: content.toString(),
data: {},
} satisfies Code);
return;
}
const parsed = processor.parse(matter(content).content);
const parsed = processor.parse(matter(content).content);
await update(parsed as Root, targetPath, processor, compiler);
Object.assign(node, parsed);
})
.catch((e) => {
console.warn(`failed to read file: ${targetPath}`, e);
}),
);
await update(parsed as Root, targetPath, processor, compiler);
Object.assign(
parent && parent.type === 'paragraph' ? parent : node,
parsed,
);
})
.catch((e) => {
console.warn(`failed to read file: ${targetPath}`, e);
}),
);
return 'skip';
});
return 'skip';
},
);
await Promise.all(queue);
}
@@ -4,14 +4,17 @@ import { SearchIcon } from 'lucide-react';
import { useSearchContext } from '@/contexts/search';
import { useI18n } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { buttonVariants } from '@/components/ui/button';
import { type ButtonProps, buttonVariants } from '@/components/ui/button';
export function SearchToggle({
hideIfDisabled,
size = 'icon',
color = 'ghost',
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & {
hideIfDisabled?: boolean;
}) {
}: ButtonHTMLAttributes<HTMLButtonElement> &
ButtonProps & {
hideIfDisabled?: boolean;
}) {
const { setOpenSearch, enabled } = useSearchContext();
if (hideIfDisabled && !enabled) return null;
@@ -20,8 +23,8 @@ export function SearchToggle({
type="button"
className={cn(
buttonVariants({
size: 'icon',
color: 'ghost',
size,
color,
}),
props.className,
)}
+3 -1
View File
@@ -1,4 +1,4 @@
import { cva } from 'class-variance-authority';
import { cva, type VariantProps } from 'class-variance-authority';
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50',
@@ -20,3 +20,5 @@ export const buttonVariants = cva(
},
},
);
export type ButtonProps = VariantProps<typeof buttonVariants>;
+24 -1
View File
@@ -1,12 +1,14 @@
'use client';
import { Menu, X } from 'lucide-react';
import { Menu, SidebarIcon, X } from 'lucide-react';
import { type ButtonHTMLAttributes, type HTMLAttributes } from 'react';
import { cn } from '@/utils/cn';
import { buttonVariants } from '@/components/ui/button';
import { useSidebar } from '@/contexts/sidebar';
import { useNav } from '@/components/layout/nav';
import { SidebarTrigger } from 'fumadocs-core/sidebar';
import { SidebarCollapseTrigger } from '@/layouts/docs/sidebar';
import { SearchToggle } from '@/components/layout/search-toggle';
export function Navbar(props: HTMLAttributes<HTMLElement>) {
const { open } = useSidebar();
@@ -47,3 +49,24 @@ export function NavbarSidebarTrigger(
</SidebarTrigger>
);
}
export function CollapsibleControl() {
const { collapsed } = useSidebar();
if (!collapsed) return;
return (
<div className="fixed flex flex-row items-center animate-fd-fade-in rounded-xl p-0.5 border bg-fd-muted text-fd-muted-foreground z-10 top-12 shadow-md max-md:hidden xl:top-4 xl:start-4 max-xl:end-4">
<SidebarCollapseTrigger
className={cn(
buttonVariants({
color: 'ghost',
size: 'icon-sm',
}),
)}
>
<SidebarIcon />
</SidebarCollapseTrigger>
<SearchToggle size="icon-sm" />
</div>
);
}
+10 -16
View File
@@ -1,7 +1,7 @@
import type { PageTree } from 'fumadocs-core/server';
import { type ReactNode, type HTMLAttributes } from 'react';
import Link from 'next/link';
import { ChevronRight, Languages } from 'lucide-react';
import { Languages, SidebarIcon } from 'lucide-react';
import { cn } from '@/utils/cn';
import { buttonVariants } from '@/components/ui/button';
import {
@@ -21,7 +21,11 @@ import {
LanguageToggle,
LanguageToggleText,
} from '@/components/layout/language-toggle';
import { Navbar, NavbarSidebarTrigger } from '@/layouts/docs.client';
import {
CollapsibleControl,
Navbar,
NavbarSidebarTrigger,
} from '@/layouts/docs.client';
import { TreeContextProvider } from '@/contexts/tree';
import { NavProvider, Title } from '@/components/layout/nav';
import { ThemeToggle } from '@/components/layout/theme-toggle';
@@ -114,19 +118,7 @@ export function DocsLayout({
...props.containerProps?.style,
}}
>
{collapsible ? (
<SidebarCollapseTrigger
className={cn(
buttonVariants({
color: 'secondary',
size: 'icon',
}),
'fixed top-1/2 -translate-y-1/2 start-0 z-40 text-fd-muted-foreground border-s-0 rounded-s-none shadow-md data-[collapsed=false]:hidden max-md:hidden',
)}
>
<ChevronRight />
</SidebarCollapseTrigger>
) : null}
{collapsible ? <CollapsibleControl /> : null}
{replaceOrDefault(
{ enabled: sidebarEnabled, component: sidebarReplace },
<Aside
@@ -151,7 +143,9 @@ export function DocsLayout({
}),
'ms-auto mb-auto text-fd-muted-foreground max-md:hidden',
)}
/>
>
<SidebarIcon />
</SidebarCollapseTrigger>
)}
</div>
{sidebarBanner}
+29 -29
View File
@@ -1,5 +1,5 @@
'use client';
import { ChevronDown, ExternalLink, SidebarIcon } from 'lucide-react';
import { ChevronDown, ExternalLink } from 'lucide-react';
import * as Base from 'fumadocs-core/sidebar';
import { usePathname } from 'next/navigation';
import {
@@ -7,9 +7,7 @@ import {
createContext,
Fragment,
type HTMLAttributes,
type PointerEventHandler,
type ReactNode,
useCallback,
useContext,
useMemo,
useRef,
@@ -89,32 +87,33 @@ export function CollapsibleSidebar(props: SidebarProps) {
closeTimeRef.current = Date.now() + 150;
});
const onEnter: PointerEventHandler = useCallback((e) => {
if (e.pointerType === 'touch' || closeTimeRef.current > Date.now()) return;
window.clearTimeout(timerRef.current);
setHover(true);
}, []);
const onLeave: PointerEventHandler = useCallback((e) => {
if (e.pointerType === 'touch') return;
window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(
() => {
setHover(false);
closeTimeRef.current = Date.now() + 150;
},
Math.min(e.clientX, document.body.clientWidth - e.clientX) > 100
? 0
: 500,
);
}, []);
return (
<Sidebar
{...props}
onPointerEnter={collapsed ? onEnter : undefined}
onPointerLeave={collapsed ? onLeave : undefined}
onPointerEnter={(e) => {
if (
!collapsed ||
e.pointerType === 'touch' ||
closeTimeRef.current > Date.now()
)
return;
window.clearTimeout(timerRef.current);
setHover(true);
}}
onPointerLeave={(e) => {
if (!collapsed || e.pointerType === 'touch') return;
window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(
() => {
setHover(false);
closeTimeRef.current = Date.now() + 150;
},
Math.min(e.clientX, document.body.clientWidth - e.clientX) > 100
? 0
: 500,
);
}}
data-collapsed={collapsed}
className={cn(
'md:transition-all',
@@ -154,7 +153,7 @@ export function Sidebar({
blockScrollingWidth={768} // md
{...props}
className={cn(
'fixed top-[calc(var(--fd-banner-height)+var(--fd-nav-height))] z-30 bg-fd-card text-sm md:sticky md:h-(--fd-sidebar-height)',
'fixed top-[calc(var(--fd-banner-height)+var(--fd-nav-height))] z-20 bg-fd-card text-sm md:sticky md:h-(--fd-sidebar-height)',
'max-md:inset-x-0 max-md:bottom-0 max-md:bg-fd-background/80 max-md:text-[15px] max-md:backdrop-blur-lg max-md:data-[open=false]:invisible',
props.className,
)}
@@ -388,7 +387,7 @@ export function SidebarCollapseTrigger(
setCollapsed((prev) => !prev);
}}
>
{props.children ?? <SidebarIcon />}
{props.children}
</button>
);
}
@@ -482,7 +481,8 @@ function PageTreeFolder({
return (
<SidebarFolder
defaultOpen={
(item.defaultOpen ?? defaultOpenLevel >= level) || path.includes(item)
(item.defaultOpen ?? defaultOpenLevel >= level) ||
path.some((pathItem) => pathItem.$id === item.$id || pathItem === item)
}
>
{item.index ? (
+10 -4
View File
@@ -22,7 +22,7 @@ import {
import { cn } from '@/utils/cn';
import Link from 'next/link';
import { buttonVariants } from '@/components/ui/button';
import { ChevronDown, Languages } from 'lucide-react';
import { ChevronDown, Languages, SidebarIcon } from 'lucide-react';
import { BaseLinkItem, type LinkItemType } from '@/layouts/links';
import { LanguageToggle } from '@/components/layout/language-toggle';
import { ThemeToggle } from '@/components/layout/theme-toggle';
@@ -143,7 +143,9 @@ export function DocsLayout({
}),
'text-fd-muted-foreground mb-auto',
)}
/>
>
<SidebarIcon />
</SidebarCollapseTrigger>
</div>
)}
{nav.children}
@@ -252,7 +254,9 @@ function DocsNavbar({
}),
'text-fd-muted-foreground -ms-1.5 me-2 data-[collapsed=false]:hidden max-md:hidden',
)}
/>
>
<SidebarIcon />
</SidebarCollapseTrigger>
) : null}
<Title
url={nav.url}
@@ -320,7 +324,9 @@ function DocsNavbar({
}),
'ms-2 text-fd-muted-foreground rounded-full max-md:hidden',
)}
/>
>
<SidebarIcon />
</SidebarCollapseTrigger>
) : null}
</div>
</div>