mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
UI: Improve sidebar UI
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Improve sidebar UI
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user