import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { VariantProps, cva } from "class-variance-authority"; import { PanelLeft } from "lucide-react"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; const SIDEBAR_COOKIE_NAME = "sidebar:state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH_DEFAULT = "16rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_WIDTH_MIN = 192; // 12rem const SIDEBAR_WIDTH_MAX = 448; // 28rem const SIDEBAR_KEYBOARD_SHORTCUT = "b"; const SIDEBAR_WIDTH_STORAGE_KEY = "sidebar:width"; type SidebarContext = { state: "expanded" | "collapsed"; open: boolean; setOpen: (open: boolean) => void; openMobile: boolean; setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; sidebarWidth: string; }; const SidebarContext = React.createContext(null); function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { throw new Error("useSidebar must be used within a SidebarProvider."); } return context; } const SidebarProvider = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; } >(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false); const [_open, _setOpen] = React.useState(defaultOpen); const open = openProp ?? _open; const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { const openState = typeof value === "function" ? value(open) : value; if (setOpenProp) { setOpenProp(openState); } else { _setOpen(openState); } document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, [setOpenProp, open], ); // Resizable width state const [sidebarWidth, setSidebarWidth] = React.useState(() => { try { return localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY) || SIDEBAR_WIDTH_DEFAULT; } catch { return SIDEBAR_WIDTH_DEFAULT; } }); const toggleSidebar = React.useCallback(() => { return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); }, [isMobile, setOpen, setOpenMobile]); React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { event.preventDefault(); toggleSidebar(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [toggleSidebar]); const state = open ? "expanded" : "collapsed"; const contextValue = React.useMemo( () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, sidebarWidth, }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, sidebarWidth], ); // Drag resize handler const isDragging = React.useRef(false); const handleMouseDown = React.useCallback((e: React.MouseEvent) => { if (state === "collapsed") return; e.preventDefault(); isDragging.current = true; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; const onMouseMove = (ev: MouseEvent) => { if (!isDragging.current) return; const newWidth = Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, ev.clientX)); const widthStr = `${newWidth}px`; setSidebarWidth(widthStr); try { localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, widthStr); } catch { /* Storage may be unavailable in private mode. */ } }; const onMouseUp = () => { isDragging.current = false; document.body.style.cursor = ""; document.body.style.userSelect = ""; document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }, [state]); return (
{children} {/* Drag resize handle, rendered at provider level to be sibling of sidebar */} {state === "expanded" && !isMobile && (
)}
); }); SidebarProvider.displayName = "SidebarProvider"; const Sidebar = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & { side?: "left" | "right"; variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none"; } >(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => { const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === "none") { return (
{children}
); } if (isMobile) { return (
{children}
); } return (
{/* This is what handles the sidebar gap on desktop */}
); }); Sidebar.displayName = "Sidebar"; const SidebarTrigger = React.forwardRef, React.ComponentProps>( ({ className, onClick, ...props }, ref) => { const { toggleSidebar } = useSidebar(); return ( ); }, ); SidebarTrigger.displayName = "SidebarTrigger"; const SidebarRail = React.forwardRef>( ({ className, ...props }, ref) => { const { toggleSidebar } = useSidebar(); return (