fumadocs-ui
Version:
The Radix UI version of Fumadocs UI
268 lines (267 loc) • 13 kB
JavaScript
"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 };