fumadocs-ui
Version:
The Radix UI version of Fumadocs UI
262 lines (259 loc) • 8.04 kB
JavaScript
'use client';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
import { ScrollArea, ScrollViewport } from "../ui/scroll-area.js";
import Link from "fumadocs-core/link";
import { usePathname } from "fumadocs-core/framework";
import { cn } from "@fumadocs/ui/cn";
import { jsx, jsxs } from "react/jsx-runtime";
import { ChevronDown, ExternalLink } from "lucide-react";
import { createContext, use, useEffect, useMemo, useRef, useState } from "react";
import { isActive } from "@fumadocs/ui/urls";
import { useOnChange } from "fumadocs-core/utils/use-on-change";
import { useMediaQuery } from "fumadocs-core/utils/use-media-query";
import { Presence } from "@radix-ui/react-presence";
import scrollIntoView from "scroll-into-view-if-needed";
//#region src/components/sidebar/base.tsx
const SidebarContext = createContext(null);
const FolderContext = createContext(null);
function SidebarProvider({ defaultOpenLevel = 0, prefetch = true, children }) {
const closeOnRedirect = useRef(true);
const [open, setOpen] = useState(false);
const [collapsed, setCollapsed] = useState(false);
const pathname = usePathname();
const mode = useMediaQuery("(width < 768px)") ? "drawer" : "full";
useOnChange(pathname, () => {
if (closeOnRedirect.current) setOpen(false);
closeOnRedirect.current = true;
});
return /* @__PURE__ */ jsx(SidebarContext, {
value: useMemo(() => ({
open,
setOpen,
collapsed,
setCollapsed,
closeOnRedirect,
defaultOpenLevel,
prefetch,
mode
}), [
open,
collapsed,
defaultOpenLevel,
prefetch,
mode
]),
children
});
}
function useSidebar() {
const ctx = use(SidebarContext);
if (!ctx) throw new Error("Missing SidebarContext, make sure you have wrapped the component in <DocsLayout /> and the context is available.");
return ctx;
}
function useFolder() {
return use(FolderContext);
}
function useFolderDepth() {
return use(FolderContext)?.depth ?? 0;
}
function SidebarContent({ children }) {
const { collapsed, mode } = useSidebar();
const [hover, setHover] = useState(false);
const ref = useRef(null);
const timerRef = useRef(0);
useOnChange(collapsed, () => {
if (collapsed) setHover(false);
});
if (mode !== "full") return;
function shouldIgnoreHover(e) {
const element = ref.current;
if (!element) return true;
return !collapsed || e.pointerType === "touch" || element.getAnimations().length > 0;
}
return children({
ref,
collapsed,
hovered: hover,
onPointerEnter(e) {
if (shouldIgnoreHover(e)) return;
window.clearTimeout(timerRef.current);
setHover(true);
},
onPointerLeave(e) {
if (shouldIgnoreHover(e)) return;
window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => setHover(false), Math.min(e.clientX, document.body.clientWidth - e.clientX) > 100 ? 0 : 500);
}
});
}
function SidebarDrawerOverlay(props) {
const { open, setOpen, mode } = useSidebar();
if (mode !== "drawer") return;
return /* @__PURE__ */ jsx(Presence, {
present: open,
children: /* @__PURE__ */ jsx("div", {
"data-state": open ? "open" : "closed",
onClick: () => setOpen(false),
...props
})
});
}
function SidebarDrawerContent({ className, children, ...props }) {
const { open, mode } = useSidebar();
const state = open ? "open" : "closed";
if (mode !== "drawer") return;
return /* @__PURE__ */ jsx(Presence, {
present: open,
children: ({ present }) => /* @__PURE__ */ jsx("aside", {
id: "nd-sidebar-mobile",
"data-state": state,
className: cn(!present && "invisible", className),
...props,
children
})
});
}
function SidebarViewport(props) {
return /* @__PURE__ */ jsx(ScrollArea, {
...props,
className: cn("min-h-0 flex-1", props.className),
children: /* @__PURE__ */ jsx(ScrollViewport, {
className: "p-4 overscroll-contain",
style: { maskImage: "linear-gradient(to bottom, transparent, white 12px, white calc(100% - 12px), transparent)" },
children: props.children
})
});
}
function SidebarSeparator(props) {
const depth = useFolderDepth();
return /* @__PURE__ */ jsx("p", {
...props,
className: cn("inline-flex items-center gap-2 mb-1.5 px-2 mt-6 empty:mb-0", depth === 0 && "first:mt-0", props.className),
children: props.children
});
}
function SidebarItem({ icon, children, ...props }) {
const pathname = usePathname();
const ref = useRef(null);
const { prefetch } = useSidebar();
const active = props.href !== void 0 && isActive(props.href, pathname, false);
useAutoScroll(active, ref);
return /* @__PURE__ */ jsxs(Link, {
ref,
"data-active": active,
prefetch,
...props,
children: [icon ?? (props.external ? /* @__PURE__ */ jsx(ExternalLink, {}) : null), children]
});
}
function SidebarFolder({ defaultOpen: defaultOpenProp, collapsible = true, active = false, children, ...props }) {
const { defaultOpenLevel } = useSidebar();
const depth = useFolderDepth() + 1;
const defaultOpen = collapsible === false || active || (defaultOpenProp ?? defaultOpenLevel >= depth);
const [open, setOpen] = useState(defaultOpen);
useOnChange(defaultOpen, (v) => {
if (v) setOpen(v);
});
return /* @__PURE__ */ jsx(Collapsible, {
open,
onOpenChange: setOpen,
disabled: !collapsible,
...props,
children: /* @__PURE__ */ jsx(FolderContext, {
value: useMemo(() => ({
open,
setOpen,
depth,
collapsible
}), [
collapsible,
depth,
open
]),
children
})
});
}
function SidebarFolderTrigger({ children, ...props }) {
const { open, collapsible } = use(FolderContext);
if (collapsible) return /* @__PURE__ */ jsxs(CollapsibleTrigger, {
...props,
children: [children, /* @__PURE__ */ jsx(ChevronDown, {
"data-icon": true,
className: cn("ms-auto transition-transform", !open && "-rotate-90")
})]
});
return /* @__PURE__ */ jsx("div", {
...props,
children
});
}
function SidebarFolderLink({ children, ...props }) {
const ref = useRef(null);
const { open, setOpen, collapsible } = use(FolderContext);
const { prefetch } = useSidebar();
const pathname = usePathname();
const active = props.href !== void 0 && isActive(props.href, pathname, false);
useAutoScroll(active, ref);
return /* @__PURE__ */ jsxs(Link, {
ref,
"data-active": active,
onClick: (e) => {
if (!collapsible) return;
if (e.target instanceof Element && e.target.matches("[data-icon], [data-icon] *")) {
setOpen(!open);
e.preventDefault();
} else setOpen(active ? !open : true);
},
prefetch,
...props,
children: [children, collapsible && /* @__PURE__ */ jsx(ChevronDown, {
"data-icon": true,
className: cn("ms-auto transition-transform", !open && "-rotate-90")
})]
});
}
function SidebarFolderContent(props) {
return /* @__PURE__ */ jsx(CollapsibleContent, {
...props,
children: props.children
});
}
function SidebarTrigger({ children, ...props }) {
const { setOpen } = useSidebar();
return /* @__PURE__ */ jsx("button", {
"aria-label": "Open Sidebar",
onClick: () => setOpen((prev) => !prev),
...props,
children
});
}
function SidebarCollapseTrigger(props) {
const { collapsed, setCollapsed } = useSidebar();
return /* @__PURE__ */ jsx("button", {
type: "button",
"aria-label": "Collapse Sidebar",
"data-collapsed": collapsed,
onClick: () => {
setCollapsed((prev) => !prev);
},
...props,
children: props.children
});
}
function useAutoScroll(active, ref) {
const { mode } = useSidebar();
useEffect(() => {
if (active && ref.current) scrollIntoView(ref.current, {
boundary: document.getElementById(mode === "drawer" ? "nd-sidebar-mobile" : "nd-sidebar"),
scrollMode: "if-needed"
});
}, [
active,
mode,
ref
]);
}
//#endregion
export { SidebarCollapseTrigger, SidebarContent, SidebarDrawerContent, SidebarDrawerOverlay, SidebarFolder, SidebarFolderContent, SidebarFolderLink, SidebarFolderTrigger, SidebarItem, SidebarProvider, SidebarSeparator, SidebarTrigger, SidebarViewport, useFolder, useFolderDepth, useSidebar };
//# sourceMappingURL=base.js.map