'use client' import { Slot } from '@radix-ui/react-slot' import clsx from 'clsx' import { err, ok, type Result } from 'neverthrow' import { type ComponentProps, createContext, type ElementRef, type Ref, useCallback, useContext, useEffect, useMemo, useState, } from 'react' import { PanelLeft } from '../../icons' import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger, } from '../Tooltip' import styles from './Sidebar.module.css' const SIDEBAR_WIDTH = '16rem' const SIDEBAR_WIDTH_ICON = '3rem' const SIDEBAR_KEYBOARD_SHORTCUT = 'b' export type SidebarState = 'expanded' | 'collapsed' type SidebarContext = { state: SidebarState open: boolean toggleSidebar: () => void } type CSSPropertiesWithCustomProps = React.CSSProperties & { '--sidebar-width'?: string '--sidebar-width-icon'?: string } const SidebarContext = createContext(null) function useSidebar(): Result { const context = useContext(SidebarContext) if (!context) { return err(new Error('useSidebar must be used within a SidebarProvider.')) } return ok(context) } const SidebarProvider = ({ open, onOpenChange, className, style, children, ref, ...props }: ComponentProps<'div'> & { open: boolean onOpenChange?: (nextPanelState: boolean) => void ref?: Ref }) => { const [openMobile, setOpenMobile] = useState(false) // Helper to toggle the sidebar. const toggleSidebar = useCallback(() => { onOpenChange?.(!open) }, [onOpenChange, open]) // Adds a keyboard shortcut to toggle the sidebar. 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 = useMemo( () => ({ state, openMobile, setOpenMobile, toggleSidebar, open, }), [state, openMobile, toggleSidebar, open], ) const sidebarStyle: CSSPropertiesWithCustomProps = { '--sidebar-width': SIDEBAR_WIDTH, '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, ...style, } return (
{children}
) } SidebarProvider.displayName = 'SidebarProvider' const Sidebar = ({ collapsible = 'offcanvas', className, children, ref, ...props }: ComponentProps<'div'> & { collapsible?: 'offcanvas' | 'icon' | 'none' ref?: Ref }) => { const sidebarResult = useSidebar() if (sidebarResult.isErr()) { throw sidebarResult.error } const { state } = sidebarResult.value if (collapsible === 'none') { return (
{children}
) } return (
{/* This is what handles the sidebar gap on desktop */}
{children}
) } Sidebar.displayName = 'Sidebar' type SidebarTriggerProps = Omit, 'onClick'> & { onClick?: (state: SidebarState) => void ref?: Ref> } const SidebarTrigger = ({ className, onClick, ref, ...props }: SidebarTriggerProps) => { const sidebarResult = useSidebar() if (sidebarResult.isErr()) { throw sidebarResult.error } const { toggleSidebar, state } = sidebarResult.value return ( {state === 'collapsed' ? 'Expand' : 'Collapse'} ) } SidebarTrigger.displayName = 'SidebarTrigger' const SidebarHeader = ({ className, ref, ...props }: ComponentProps<'div'> & { ref?: Ref }) => { return
} SidebarHeader.displayName = 'SidebarHeader' const SidebarFooter = ({ className, ref, ...props }: ComponentProps<'div'> & { ref?: Ref }) => { return (
) } SidebarFooter.displayName = 'SidebarFooter' const SidebarContent = ({ className, ref, ...props }: ComponentProps<'div'> & { ref?: Ref }) => { return (
) } SidebarContent.displayName = 'SidebarContent' const SidebarGroup = ({ className, ref, ...props }: ComponentProps<'div'> & { ref?: Ref }) => { return (
) } SidebarGroup.displayName = 'SidebarGroup' const SidebarGroupLabel = ({ className, children, asChild = false, ref, ...props }: ComponentProps<'div'> & { asChild?: boolean ref?: Ref }) => { const Comp = asChild ? Slot : 'div' return ( {children} ) } SidebarGroupLabel.displayName = 'SidebarGroupLabel' const SidebarGroupAction = ({ className, asChild = false, ref, ...props }: ComponentProps<'button'> & { asChild?: boolean ref?: Ref }) => { const Comp = asChild ? Slot : 'button' return } SidebarGroupAction.displayName = 'SidebarGroupAction' const SidebarGroupContent = ({ className, ref, ...props }: ComponentProps<'div'> & { ref?: Ref }) => (
) SidebarGroupContent.displayName = 'SidebarGroupContent' const SidebarMenu = ({ ref, ...props }: ComponentProps<'ul'> & { ref?: Ref }) =>
    SidebarMenu.displayName = 'SidebarMenu' const SidebarMenuItem = ({ className, ref, ...props }: ComponentProps<'li'> & { ref?: Ref }) => (
  • ) SidebarMenuItem.displayName = 'SidebarMenuItem' const SidebarMenuButton = ({ asChild = false, isActive = false, tooltip, className, showtooltip, ref, ...props }: ComponentProps<'button'> & { asChild?: boolean isActive?: boolean tooltip?: string | ComponentProps showtooltip?: boolean ref?: Ref }) => { const Comp = asChild ? Slot : 'button' const button = ( ) if (!tooltip) { return button } if (typeof tooltip === 'string') { tooltip = { children: tooltip, } } return ( {button} ) } SidebarMenuButton.displayName = 'SidebarMenuButton' const SidebarMenuAction = ({ className, asChild = false, showOnHover = false, ref, ...props }: ComponentProps<'button'> & { asChild?: boolean showOnHover?: boolean ref?: Ref }) => { const Comp = asChild ? Slot : 'button' return ( ) } SidebarMenuAction.displayName = 'SidebarMenuAction' const SidebarMenuBadge = ({ className, ref, ...props }: ComponentProps<'div'> & { ref?: Ref }) =>
    SidebarMenuBadge.displayName = 'SidebarMenuBadge' const SidebarMenuSub = ({ className, ref, ...props }: ComponentProps<'ul'> & { ref?: Ref }) =>
      SidebarMenuSub.displayName = 'SidebarMenuSub' const SidebarMenuSubItem = ({ ref, ...props }: ComponentProps<'li'> & { ref?: Ref }) =>
    • SidebarMenuSubItem.displayName = 'SidebarMenuSubItem' const SidebarMenuSubButton = ({ asChild = false, size = 'md', isActive, className, ref, ...props }: ComponentProps<'a'> & { asChild?: boolean size?: 'sm' | 'md' isActive?: boolean ref?: Ref }) => { const Comp = asChild ? Slot : 'a' return ( ) } SidebarMenuSubButton.displayName = 'SidebarMenuSubButton' export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarTrigger, useSidebar, }