UNPKG

fumadocs-ui

Version:

The Radix UI version of Fumadocs UI

268 lines (267 loc) 13 kB
"use client"; import { cn } from "../../../utils/cn.js"; import { buttonVariants } from "../../../components/ui/button.js"; import { useTreeContext, useTreePath } from "../../../contexts/tree.js"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../components/ui/collapsible.js"; import { isLinkItemActive } from "../../shared/index.js"; import { ScrollArea, ScrollViewport } from "../../../components/ui/scroll-area.js"; import { SidebarDrawerContent, SidebarDrawerOverlay, SidebarProvider as SidebarProvider$1, useSidebar as useSidebar$1 } from "../../../components/sidebar/base.js"; import { LayoutTabsDropdown } from "../layout-tabs.js"; import { useGlassLayout } from "../index.js"; import Link from "fumadocs-core/link"; import { usePathname } from "fumadocs-core/framework"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { ChevronDown, ChevronsUpDown, LanguagesIcon, MessageCircleIcon, SidebarIcon, XIcon } from "lucide-react"; import { cva } from "class-variance-authority"; import { cloneElement, createContext, use, useState } from "react"; import { useTranslations } from "@fuma-translate/react"; import { useOnChange } from "fumadocs-core/utils/use-on-change"; //#region src/layouts/glass/slots/sidebar.tsx const itemTriggerVariants = cva("inline-flex text-sm items-center gap-2 rounded-lg px-2.5 py-2 md:py-1.5 [&_svg]:size-4 outline-none focus-visible:ring-2 focus-visible:ring-fd-ring", { variants: { active: { true: "bg-fd-primary/10 text-fd-primary", false: "text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-accent-foreground" } } }); const CollapsibleContext = createContext(true); function SidebarProvider({ children, collapsible = true }) { return /* @__PURE__ */ jsx(SidebarProvider$1, { children: /* @__PURE__ */ jsx(CollapsibleContext, { value: collapsible, children }) }); } function useSidebar() { const collapsible = use(CollapsibleContext); const { collapsed, setCollapsed } = useSidebar$1(); return { collapsible, collapsed, setCollapsed }; } function SidebarDrawer({ contentProps }) { const { menuItems, props: { aiChat }, slots } = useGlassLayout(); const { root } = useTreeContext(); const { setOpen } = useSidebar$1(); const t = useTranslations(); return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SidebarDrawerOverlay, { className: "fixed z-40 inset-0 bg-fd-overlay backdrop-blur-sm data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out" }), /* @__PURE__ */ jsx(SidebarDrawerContent, { className: "fixed z-40 inset-e-0 inset-y-0 flex flex-col w-[360px] max-w-[calc(100vw-3rem)] border-s bg-fd-background text-fd-foreground shadow-md data-[state=open]:animate-fd-sidebar-in data-[state=closed]:animate-fd-sidebar-out", children: /* @__PURE__ */ jsxs("div", { ...contentProps, className: cn("flex flex-col px-3 size-full overflow-y-auto fd-scroll-container", contentProps?.className), children: [ /* @__PURE__ */ jsxs("div", { className: "sticky flex flex-col gap-2 top-0 py-2 bg-fd-background shadow-lg shadow-fd-background", children: [/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 ps-2.5", children: [ /* @__PURE__ */ jsx(slots.navTitle, { className: "flex text-sm items-center font-semibold gap-2 flex-1" }), aiChat && /* @__PURE__ */ jsxs("button", { type: "button", className: cn(buttonVariants({ variant: "secondary", size: "sm" }), "rounded-full h-8 gap-1.5"), onClick: () => { aiChat.onOpenChange(!aiChat.open); setOpen(false); }, children: [/* @__PURE__ */ jsx(MessageCircleIcon, { className: "size-4 text-fd-muted-foreground" }), t("Ask AI", { note: "AI chat button" })] }), /* @__PURE__ */ jsx("button", { type: "button", "aria-label": t("Close Sidebar", { note: "aria-label" }), onClick: () => setOpen(false), className: cn(buttonVariants({ variant: "secondary", size: "icon-sm" }), "rounded-full"), children: /* @__PURE__ */ jsx(XIcon, {}) }) ] }), slots.languageSelect && /* @__PURE__ */ jsxs(slots.languageSelect.root, { variant: "secondary", className: "px-2.5 gap-2 rounded-lg", children: [ /* @__PURE__ */ jsx(LanguagesIcon, { className: "size-4 text-fd-muted-foreground shrink-0" }), /* @__PURE__ */ jsx(slots.languageSelect.text, {}), /* @__PURE__ */ jsx(ChevronsUpDown, { className: "ms-auto size-3.5 text-fd-muted-foreground shrink-0" }) ] })] }), /* @__PURE__ */ jsxs("div", { className: "flex flex-col py-2 flex-1", children: [menuItems.map((item, i) => item.type !== "icon" && /* @__PURE__ */ jsx(SidebarLinkItem, { item }, i)), root.children.map((item, i) => cloneElement(renderNode(item), { key: i }))] }), /* @__PURE__ */ jsxs("div", { className: "flex items-center sticky bottom-0 bg-fd-background px-1 pt-2 pb-4 border-t mt-2 empty:hidden", children: [menuItems.map((item, i) => item.type === "icon" && /* @__PURE__ */ jsx(SidebarIconLinkItem, { item }, i)), slots.themeSwitch && /* @__PURE__ */ jsx(slots.themeSwitch, { className: "p-0 ms-auto" })] }) ] }) })] }); } function Sidebar({ className, children, ...props }) { const { menuItems, props: { tabs }, slots } = useGlassLayout(); const { root } = useTreeContext(); const { collapsible, collapsed, setCollapsed } = useSidebar(); const t = useTranslations({ note: "sidebar" }); return /* @__PURE__ */ jsxs("aside", { id: "nd-sidebar", className: cn("sticky flex flex-col transition-transform [grid-area:left] my-2 ms-2 z-30 top-2 border rounded-2xl bg-fd-popover/80 text-fd-popover-foreground backdrop-blur-sm shadow-sm h-[calc(100dvh---spacing(4))] max-md:hidden md:layout:[--fd-left-width:280px]", collapsed && "w-[calc(280px---spacing(2))] -translate-x-[280px] md:layout:[--fd-left-width:0px]", className), ...props, children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1 pt-4 ps-4.5 pe-3.5 empty:hidden", children: [/* @__PURE__ */ jsx(slots.navTitle, { className: "flex text-sm items-center font-semibold gap-2 me-auto" }), /* @__PURE__ */ jsxs("div", { className: "flex -mt-1.5 -me-1.5 empty:hidden", children: [menuItems.map((item, i) => item.type === "icon" && /* @__PURE__ */ jsx(SidebarIconLinkItem, { item }, i)), collapsible && /* @__PURE__ */ jsx("button", { "aria-label": collapsed ? t("Show Sidebar") : t("Hide Sidebar"), className: cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), "text-fd-muted-foreground"), onClick: () => setCollapsed(!collapsed), children: /* @__PURE__ */ jsx(SidebarIcon, {}) })] })] }), tabs.length > 0 && /* @__PURE__ */ jsx(LayoutTabsDropdown, { tabs, className: "min-w-0 bg-fd-secondary text-fd-secondary-foreground rounded-xl px-2.5 py-2 [&_svg]:size-4 border rounded-xl shadow-sm mx-2 mt-2.5 empty:hidden" }), /* @__PURE__ */ jsx(ScrollArea, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxs(ScrollViewport, { className: "flex flex-col p-2 [mask-image:linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent))]", children: [menuItems.map((item, i) => item.type !== "icon" && /* @__PURE__ */ jsx(SidebarLinkItem, { item }, i)), root.children.map((item, i) => cloneElement(renderNode(item), { key: i }))] }) }), children ] }); } function SidebarIconLinkItem({ item, className }) { return /* @__PURE__ */ jsx(Link, { href: item.url, external: item.external, "aria-label": item.label, className: cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), "text-fd-muted-foreground", className), children: item.icon }); } function SidebarLinkItem({ item, className }) { const pathname = usePathname(); const [open, setOpen] = useState(true); switch (item.type) { case "custom": return /* @__PURE__ */ jsx("div", { className, children: item.children }); case "menu": { const rightIcon = /* @__PURE__ */ jsx(ChevronDown, { className: cn("ms-auto text-fd-muted-foreground size-3.5! transition-transform", !open && "-rotate-90") }); return /* @__PURE__ */ jsxs(Collapsible, { open, onOpenChange: setOpen, className: cn("mt-4 first:mt-0", className), children: [item.url ? /* @__PURE__ */ jsxs("div", { className: "flex w-full text-sm px-2.5 py-1.5 font-medium", children: [/* @__PURE__ */ jsxs(Link, { href: item.url, external: item.external, className: cn("inline-flex items-center gap-2 [&_svg]:size-4", isLinkItemActive(item, pathname) ? "text-fd-primary" : "hover:underline hover:decoration-fd-muted-foreground hover:underline-offset-4 hover:decoration-dashed hover:text-fd-accent-foreground"), children: [item.icon, item.text] }), /* @__PURE__ */ jsx(CollapsibleTrigger, { className: "flex-1", children: rightIcon })] }) : /* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "w-full text-sm px-2.5 py-1.5 font-medium inline-flex items-center gap-2 [&_svg]:size-4", children: [ item.icon, item.text, rightIcon ] }), /* @__PURE__ */ jsx(CollapsibleContent, { className: "flex flex-col", children: item.items.map((item, i) => /* @__PURE__ */ jsx(SidebarLinkItem, { item }, i)) })] }); } default: return /* @__PURE__ */ jsxs(Link, { href: item.url, external: item.external, className: cn(itemTriggerVariants({ active: isLinkItemActive(item, pathname), className })), children: [item.icon, item.text] }); } } function renderNode(node) { if (node.type === "page") return /* @__PURE__ */ jsx(SidebarItem, { item: node }); if (node.type === "folder") return /* @__PURE__ */ jsx(SidebarFolder, { folder: node }); return /* @__PURE__ */ jsxs("p", { className: "mt-4 w-full text-sm px-2.5 py-1.5 font-medium inline-flex items-center gap-2 [&_svg]:size-4 empty:hidden first:mt-0", children: [node.icon, node.name] }); } function SidebarItem({ item }) { const path = useTreePath(); return /* @__PURE__ */ jsxs(Link, { href: item.url, external: item.external, className: itemTriggerVariants({ active: isNodeInPath(item, path) }), children: [item.icon, item.name] }); } function SidebarFolder({ folder }) { const path = useTreePath(); const shouldOpen = (folder.defaultOpen ?? true) || isNodeInPath(folder, path); const [open, setOpen] = useState(shouldOpen); useOnChange(shouldOpen, () => shouldOpen && setOpen(true)); if (folder.collapsible === false) return /* @__PURE__ */ jsxs(Fragment, { children: [folder.index ? /* @__PURE__ */ jsxs(Link, { href: folder.index.url, external: folder.index.external, className: cn("inline-flex text-sm px-2.5 py-1.5 font-medium items-center gap-2 [&_svg]:size-4 mt-4 first:mt-0", isNodeInPath(folder.index, path) ? "text-fd-primary" : "hover:underline hover:decoration-fd-muted-foreground hover:underline-offset-4 hover:decoration-dashed hover:text-fd-accent-foreground"), children: [folder.icon, folder.name] }) : /* @__PURE__ */ jsxs("div", { className: "w-full text-sm px-3 py-1.5 font-medium inline-flex items-center gap-2 [&_svg]:size-4 mt-4 first:mt-0", children: [folder.icon, folder.name] }), folder.children.map((item, i) => cloneElement(renderNode(item), { key: i }))] }); const rightIcon = /* @__PURE__ */ jsx(ChevronDown, { className: cn("ms-auto text-fd-muted-foreground size-3.5! transition-transform", !open && "-rotate-90") }); return /* @__PURE__ */ jsxs(Collapsible, { open, onOpenChange: setOpen, className: "mt-4 first:mt-0", children: [folder.index ? /* @__PURE__ */ jsxs("div", { className: "flex w-full text-sm px-2.5 py-1.5 font-medium", children: [/* @__PURE__ */ jsxs(Link, { href: folder.index.url, external: folder.index.external, className: cn("inline-flex items-center gap-2 [&_svg]:size-4", isNodeInPath(folder.index, path) ? "text-fd-primary" : "hover:underline hover:decoration-fd-muted-foreground hover:underline-offset-4 hover:decoration-dashed hover:text-fd-accent-foreground"), children: [folder.icon, folder.name] }), /* @__PURE__ */ jsx(CollapsibleTrigger, { className: "flex-1", children: rightIcon })] }) : /* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "w-full text-sm px-2.5 py-1.5 font-medium inline-flex items-center gap-2 [&_svg]:size-4", children: [ folder.icon, folder.name, rightIcon ] }), /* @__PURE__ */ jsx(CollapsibleContent, { className: "flex flex-col", children: folder.children.map((item, i) => cloneElement(renderNode(item), { key: i })) })] }); } function isNodeInPath(node, path) { return path.some((other) => other.$id === node.$id || other === node); } //#endregion export { Sidebar, SidebarDrawer, SidebarProvider, useSidebar };