fumadocs-ui
Version:
The framework for building a documentation website in Next.js
203 lines (202 loc) • 11.8 kB
JavaScript
'use client';
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { ChevronDown, ExternalLink } from '../../icons.js';
import { usePathname } from 'fumadocs-core/framework';
import { createContext, Fragment, useContext, useMemo, useRef, useState, } from 'react';
import Link from 'fumadocs-core/link';
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
import { cn } from '../../utils/cn.js';
import { ScrollArea, ScrollViewport } from '../../components/ui/scroll-area.js';
import { isActive } from '../../utils/is-active.js';
import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '../../components/ui/collapsible.js';
import { useSidebar } from '../../contexts/sidebar.js';
import { cva } from 'class-variance-authority';
import { useTreeContext, useTreePath } from '../../contexts/tree.js';
import { useMediaQuery } from 'fumadocs-core/utils/use-media-query';
import { Presence } from '@radix-ui/react-presence';
const itemVariants = cva('relative flex flex-row items-center gap-2 rounded-xl p-2 ps-(--sidebar-item-offset) text-start text-fd-muted-foreground [overflow-wrap:anywhere] [&_svg]:size-4 [&_svg]:shrink-0', {
variants: {
active: {
true: 'bg-fd-primary/10 text-fd-primary',
false: 'transition-colors hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none',
},
},
});
const Context = createContext(null);
const FolderContext = createContext(null);
export function Sidebar({ defaultOpenLevel = 0, prefetch = true, Mobile, Content, }) {
const isMobile = useMediaQuery('(width < 768px)') ?? false;
const context = useMemo(() => {
return {
defaultOpenLevel,
prefetch,
level: 1,
};
}, [defaultOpenLevel, prefetch]);
return (_jsx(Context.Provider, { value: context, children: isMobile && Mobile != null ? Mobile : Content }));
}
export function SidebarContent(props) {
const { collapsed } = useSidebar();
const [hover, setHover] = useState(false);
const timerRef = useRef(0);
const closeTimeRef = useRef(0);
useOnChange(collapsed, () => {
setHover(false);
closeTimeRef.current = Date.now() + 150;
});
return (_jsx("aside", { id: "nd-sidebar", ...props, "data-collapsed": collapsed, className: cn('fixed start-0 flex flex-col items-end top-(--fd-sidebar-top) bottom-(--fd-sidebar-margin) z-20 bg-fd-card text-sm border-e max-md:hidden *:w-(--fd-sidebar-width)', collapsed && [
'rounded-xl border translate-x-(--fd-sidebar-offset) rtl:-translate-x-(--fd-sidebar-offset)',
hover ? 'z-50 shadow-lg' : 'opacity-0',
], props.className), style: {
transition: ['top', 'opacity', 'translate', 'width']
.map((v) => `${v} ease 250ms`)
.join(', '),
...props.style,
'--fd-sidebar-offset': hover
? 'calc(var(--spacing) * 2)'
: 'calc(16px - 100%)',
'--fd-sidebar-margin': collapsed ? '0.5rem' : '0px',
'--fd-sidebar-top': `calc(var(--fd-banner-height) + var(--fd-nav-height) + var(--fd-sidebar-margin))`,
width: collapsed
? 'var(--fd-sidebar-width)'
: 'calc(var(--fd-sidebar-width) + var(--fd-layout-offset))',
}, 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);
}, children: props.children }));
}
export function SidebarContentMobile({ className, children, ...props }) {
const { open, setOpen } = useSidebar();
const state = open ? 'open' : 'closed';
return (_jsxs(_Fragment, { children: [_jsx(Presence, { present: open, children: _jsx("div", { "data-state": state, className: "fixed z-40 inset-0 backdrop-blur-xs data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out", onClick: () => setOpen(false) }) }), _jsx(Presence, { present: open, children: ({ present }) => (_jsx("aside", { id: "nd-sidebar-mobile", ...props, "data-state": state, className: cn('fixed text-[15px] flex flex-col shadow-lg border-s end-0 inset-y-0 w-[85%] max-w-[380px] z-40 bg-fd-background data-[state=open]:animate-fd-sidebar-in data-[state=closed]:animate-fd-sidebar-out', !present && 'invisible', className), children: children })) })] }));
}
export function SidebarHeader(props) {
return (_jsx("div", { ...props, className: cn('flex flex-col gap-3 p-4 pb-2', props.className), children: props.children }));
}
export function SidebarFooter(props) {
return (_jsx("div", { ...props, className: cn('flex flex-col border-t px-4 py-3', props.className), children: props.children }));
}
export function SidebarViewport(props) {
return (_jsx(ScrollArea, { ...props, className: cn('h-full', props.className), children: _jsx(ScrollViewport, { className: "p-4", style: {
'--sidebar-item-offset': 'calc(var(--spacing) * 2)',
maskImage: 'linear-gradient(to bottom, transparent, white 12px, white calc(100% - 12px), transparent)',
}, children: props.children }) }));
}
export function SidebarSeparator(props) {
return (_jsx("p", { ...props, className: cn('inline-flex items-center gap-2 mb-1.5 px-2 ps-(--sidebar-item-offset) empty:mb-0 [&_svg]:size-4 [&_svg]:shrink-0', props.className), children: props.children }));
}
export function SidebarItem({ icon, ...props }) {
const pathname = usePathname();
const active = props.href !== undefined && isActive(props.href, pathname, false);
const { prefetch } = useInternalContext();
return (_jsxs(Link, { ...props, "data-active": active, className: cn(itemVariants({ active }), props.className), prefetch: prefetch, children: [icon ?? (props.external ? _jsx(ExternalLink, {}) : null), props.children] }));
}
export function SidebarFolder({ defaultOpen = false, ...props }) {
const [open, setOpen] = useState(defaultOpen);
useOnChange(defaultOpen, (v) => {
if (v)
setOpen(v);
});
return (_jsx(Collapsible, { open: open, onOpenChange: setOpen, ...props, children: _jsx(FolderContext.Provider, { value: useMemo(() => ({ open, setOpen }), [open]), children: props.children }) }));
}
export function SidebarFolderTrigger({ className, ...props }) {
const { open } = useFolderContext();
return (_jsxs(CollapsibleTrigger, { className: cn(itemVariants({ active: false }), 'w-full', className), ...props, children: [props.children, _jsx(ChevronDown, { "data-icon": true, className: cn('ms-auto transition-transform', !open && '-rotate-90') })] }));
}
export function SidebarFolderLink(props) {
const { open, setOpen } = useFolderContext();
const { prefetch } = useInternalContext();
const pathname = usePathname();
const active = props.href !== undefined && isActive(props.href, pathname, false);
return (_jsxs(Link, { ...props, "data-active": active, className: cn(itemVariants({ active }), 'w-full', props.className), onClick: (e) => {
if (e.target instanceof HTMLElement &&
e.target.hasAttribute('data-icon')) {
setOpen(!open);
e.preventDefault();
}
else {
setOpen(active ? !open : true);
}
}, prefetch: prefetch, children: [props.children, _jsx(ChevronDown, { "data-icon": true, className: cn('ms-auto transition-transform', !open && '-rotate-90') })] }));
}
export function SidebarFolderContent(props) {
const ctx = useInternalContext();
const level = ctx.level + 1;
return (_jsxs(CollapsibleContent, { ...props, className: cn('relative', level === 2 &&
"**:data-[active=true]:before:content-[''] **:data-[active=true]:before:bg-fd-primary **:data-[active=true]:before:absolute **:data-[active=true]:before:w-px **:data-[active=true]:before:inset-y-2.5 **:data-[active=true]:before:start-2.5", props.className), style: {
'--sidebar-item-offset': `calc(var(--spacing) * ${level > 1 ? level * 3 : 2})`,
...props.style,
}, children: [level === 2 && (_jsx("div", { className: "absolute w-px inset-y-1 bg-fd-border start-2.5" })), _jsx(Context.Provider, { value: useMemo(() => ({
...ctx,
level,
}), [ctx, level]), children: props.children })] }));
}
export function SidebarTrigger({ children, ...props }) {
const { setOpen } = useSidebar();
return (_jsx("button", { ...props, "aria-label": "Open Sidebar", onClick: () => setOpen((prev) => !prev), children: children }));
}
export function SidebarCollapseTrigger(props) {
const { collapsed, setCollapsed } = useSidebar();
return (_jsx("button", { type: "button", "aria-label": "Collapse Sidebar", "data-collapsed": collapsed, ...props, onClick: () => {
setCollapsed((prev) => !prev);
}, children: props.children }));
}
function useFolderContext() {
const ctx = useContext(FolderContext);
if (!ctx)
throw new Error('Missing sidebar folder');
return ctx;
}
function useInternalContext() {
const ctx = useContext(Context);
if (!ctx)
throw new Error('<Sidebar /> component required.');
return ctx;
}
/**
* Render sidebar items from page tree
*/
export function SidebarPageTree(props) {
const { root } = useTreeContext();
return useMemo(() => {
const { Separator, Item, Folder } = props.components ?? {};
function renderSidebarList(items, level) {
return items.map((item, i) => {
if (item.type === 'separator') {
if (Separator)
return _jsx(Separator, { item: item }, i);
return (_jsxs(SidebarSeparator, { className: cn(i !== 0 && 'mt-6'), children: [item.icon, item.name] }, i));
}
if (item.type === 'folder') {
const children = renderSidebarList(item.children, level + 1);
if (Folder)
return (_jsx(Folder, { item: item, level: level, children: children }, i));
return (_jsx(PageTreeFolder, { item: item, children: children }, i));
}
if (Item)
return _jsx(Item, { item: item }, item.url);
return (_jsx(SidebarItem, { href: item.url, external: item.external, icon: item.icon, children: item.name }, item.url));
});
}
return (_jsx(Fragment, { children: renderSidebarList(root.children, 1) }, root.$id));
}, [props.components, root]);
}
function PageTreeFolder({ item, ...props }) {
const { defaultOpenLevel, level } = useInternalContext();
const path = useTreePath();
return (_jsxs(SidebarFolder, { defaultOpen: (item.defaultOpen ?? defaultOpenLevel >= level) || path.includes(item), children: [item.index ? (_jsxs(SidebarFolderLink, { href: item.index.url, external: item.index.external, ...props, children: [item.icon, item.name] })) : (_jsxs(SidebarFolderTrigger, { ...props, children: [item.icon, item.name] })), _jsx(SidebarFolderContent, { children: props.children })] }));
}