import { Slot } from '@radix-ui/react-slot' import { PanelLeftIcon } from 'lucide-react' import * as React from 'react' import { cn } from 'shared/lib/utils' import { Button } from 'shared/ui/button' import { Input } from 'shared/ui/input' import { Separator } from 'shared/ui/separator' const SIDEBAR_COOKIE_NAME = 'sidebar_state' const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_WIDTH = '16rem' 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 } 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 } // Simple mobile hook implementation function useIsMobile() { const [isMobile, setIsMobile] = React.useState(false) React.useEffect(() => { const checkMobile = () => { setIsMobile(window.innerWidth < 768) } checkMobile() window.addEventListener('resize', checkMobile) return () => window.removeEventListener('resize', checkMobile) }, []) return isMobile } 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) // 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, }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] ) return (
{children}
) } 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 (
setOpenMobile(false)} >
e.stopPropagation()} >
{children}
) } return (
{/* This is what handles the sidebar gap on desktop */}
) } function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) { const { toggleSidebar } = useSidebar() return ( ) } function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) { return (
) } function SidebarInput({ className, ...props }: React.ComponentProps) { return ( ) } function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) } function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
) } function SidebarSeparator({ className, ...props }: React.ComponentProps) { return ( ) } function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) { return (
) } function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) { return (
) } function SidebarGroupLabel({ className, asChild = false, ...props }: React.ComponentProps<'div'> & { asChild?: boolean }) { const Comp = asChild ? Slot : 'div' return ( svg]:size-4 [&>svg]:shrink-0', 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', className )} {...props} /> ) } function SidebarGroupContent({ className, ...props }: React.ComponentProps<'div'>) { return (
) } export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarProvider, SidebarSeparator, SidebarTrigger, useSidebar, }