"use client" import * as React from "react" import { mergeProps } from "@base-ui/react/merge-props" import { useRender } from "@base-ui/react/use-render" import { cva, type VariantProps } from "class-variance-authority" 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, SheetDescription, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { Skeleton } from "@/components/ui/skeleton" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { PanelLeftIcon } from "lucide-react" const SIDEBAR_COOKIE_NAME = "sidebar_state" const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_WIDTH_DEFAULT = 256 const SIDEBAR_WIDTH_MIN = 200 const SIDEBAR_WIDTH_MAX = 360 const SIDEBAR_WIDTH_STORAGE_KEY = "sidebar_width" const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_KEYBOARD_SHORTCUT = "b" type SidebarContextProps = { state: "expanded" | "collapsed" open: boolean setOpen: (open: boolean) => void openMobile: boolean setOpenMobile: (open: boolean) => void isMobile: boolean toggleSidebar: () => void width: number setWidth: (width: number) => void isResizing: boolean setIsResizing: (v: boolean) => void } 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 } function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }: React.ComponentProps<"div"> & { defaultOpen?: boolean open?: boolean onOpenChange?: (open: boolean) => void }) { const isMobile = useIsMobile() const [openMobile, setOpenMobile] = React.useState(false) const [width, _setWidth] = React.useState(SIDEBAR_WIDTH_DEFAULT) const [isResizing, setIsResizing] = React.useState(false) React.useEffect(() => { const stored = localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY) if (stored) _setWidth(Number(stored)) }, []) const setWidth = React.useCallback((w: number) => { const clamped = Math.max(SIDEBAR_WIDTH_MIN, Math.min(SIDEBAR_WIDTH_MAX, w)) _setWidth(clamped) localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(clamped)) }, []) // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. 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) } // This sets the cookie to keep the sidebar state. document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` }, [setOpenProp, open] ) // Helper to toggle the sidebar. const toggleSidebar = React.useCallback(() => { return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) }, [isMobile, setOpen, setOpenMobile]) // Adds a keyboard shortcut to toggle the sidebar. 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]) // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. const state = open ? "expanded" : "collapsed" const contextValue = React.useMemo( () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, width, setWidth, isResizing, setIsResizing, }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, width, setWidth, isResizing, setIsResizing] ) return (
{children}
) } function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, dir, ...props }: React.ComponentProps<"div"> & { side?: "left" | "right" variant?: "sidebar" | "floating" | "inset" collapsible?: "offcanvas" | "icon" | "none" }) { const { isMobile, state, openMobile, setOpenMobile, isResizing } = useSidebar() if (collapsible === "none") { return (
{children}
) } if (isMobile) { return ( Sidebar Displays the mobile sidebar.
{children}
) } return (
{/* This is what handles the sidebar gap on desktop */}
) } function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) { const { toggleSidebar } = useSidebar() return ( ) } function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { const { toggleSidebar, setWidth, setIsResizing } = useSidebar() const didDragRef = React.useRef(false) const dragRef = React.useRef<{ startX: number; startWidth: number } | null>(null) const onMouseDown = React.useCallback( (e: React.MouseEvent) => { e.preventDefault() didDragRef.current = false const sidebarEl = (e.target as HTMLElement).closest("[data-slot='sidebar']") const containerEl = sidebarEl?.querySelector("[data-slot='sidebar-container']") if (!containerEl) return dragRef.current = { startX: e.clientX, startWidth: containerEl.getBoundingClientRect().width } setIsResizing(true) const onMouseMove = (ev: MouseEvent) => { if (!dragRef.current) return didDragRef.current = true const delta = ev.clientX - dragRef.current.startX setWidth(dragRef.current.startWidth + delta) } const onMouseUp = () => { dragRef.current = null setIsResizing(false) document.removeEventListener("mousemove", onMouseMove) document.removeEventListener("mouseup", onMouseUp) document.body.style.cursor = "" document.body.style.userSelect = "" } document.addEventListener("mousemove", onMouseMove) document.addEventListener("mouseup", onMouseUp) document.body.style.cursor = "col-resize" document.body.style.userSelect = "none" }, [setWidth, setIsResizing] ) const handleClick = React.useCallback(() => { if (!didDragRef.current) toggleSidebar() }, [toggleSidebar]) return (