"use client"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { PanelLeftIcon } from "lucide-react"; import * as React from "react"; import { create } from "zustand"; import { persist } from "zustand/middleware"; import { Button } from "@/components/ui/primitives/button"; import { Input } from "@/components/ui/primitives/input"; import { Separator } from "@/components/ui/primitives/separator"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "@/components/ui/primitives/sheet"; import { Skeleton } from "@/components/ui/primitives/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/primitives/tooltip"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "18rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_KEYBOARD_SHORTCUT = "b"; type SidebarStore = { width: number; setWidth: (width: number) => void; isDragging: boolean; setIsDragging: (isDragging: boolean) => void; }; export const useSidebarStore = create()( persist( (set) => ({ width: 288, // 18rem = 288px setWidth: (width) => set({ width: Math.max(288, Math.min(width, 800)) }), isDragging: false, setIsDragging: (isDragging) => set({ isDragging }), }), { name: "sidebar-preferences", partialize: (state) => ({ width: state.width }), // Only persist width } ) ); type SidebarContextProps = { state: "expanded" | "collapsed"; open: boolean; setOpen: (open: boolean) => void; openMobile: boolean; setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => 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 sidebarWidth = useSidebarStore((state) => state.width); const isDragging = useSidebarStore((state) => state.isDragging); // 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( () => isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open), [isMobile, setOpen], ); // 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, }), [state, open, setOpen, isMobile, openMobile, toggleSidebar], ); return (
{children}
); } function SidebarResizer({ side }: { side: "left" | "right" }) { const setWidth = useSidebarStore((state) => state.setWidth); const setIsDragging = useSidebarStore((state) => state.setIsDragging); const isResizing = React.useRef(false); const handlePointerDown = (e: React.PointerEvent) => { isResizing.current = true; setIsDragging(true); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }; React.useEffect(() => { const handlePointerMove = (e: PointerEvent) => { if (!isResizing.current) return; const newWidth = side === "left" ? e.clientX : window.innerWidth - e.clientX; setWidth(Math.max(288, Math.min(newWidth, 800))); }; const handlePointerUp = () => { isResizing.current = false; setIsDragging(false); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); return () => { window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); }; }, [setWidth, side]); return (
); } function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }: React.ComponentProps<"div"> & { side?: "left" | "right"; variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none"; }) { const { isMobile, state, openMobile, setOpenMobile } = 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 } = useSidebar(); return (