'use client'; import * as React from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Slot } from '@radix-ui/react-slot'; import { VariantProps, cva } from 'class-variance-authority'; import { ChevronDown, PanelLeft } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useIsMobile } from '../hooks/use-mobile'; import { cn, isRouteActive } from '../lib/utils'; import { If } from '../makerkit/if'; import type { SidebarConfig } from '../makerkit/sidebar'; import { Trans } from '../makerkit/trans'; import { Button } from './button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from './collapsible'; import { SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON, SIDEBAR_WIDTH_MOBILE, } from './constants'; import { Input } from './input'; import { Separator } from './separator'; import { Sheet, SheetContent } from './sheet'; import { Skeleton } from './skeleton'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from './tooltip'; const SIDEBAR_COOKIE_NAME = 'sidebar:state'; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; const SIDEBAR_MINIMIZED_WIDTH = SIDEBAR_WIDTH_ICON; type SidebarContext = { state: 'expanded' | 'collapsed'; open: boolean; setOpen: (open: boolean) => void; openMobile: boolean; setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; }; export 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.FC< React.ComponentProps<'div'> & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; } > = ({ ref, defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }) => { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false); const collapsibleStyle = process.env.NEXT_PUBLIC_SIDEBAR_COLLAPSIBLE_STYLE; // 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)) => { if (setOpenProp) { return setOpenProp?.(typeof value === 'function' ? value(open) : value); } _setOpen(value); // This sets the cookie to keep the sidebar state. document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; 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], ); const sidebarWidth = !open ? collapsibleStyle === 'icon' ? SIDEBAR_WIDTH_ICON : collapsibleStyle === 'offcanvas' ? 0 : SIDEBAR_MINIMIZED_WIDTH : SIDEBAR_WIDTH; return (
{children}
); }; SidebarProvider.displayName = 'SidebarProvider'; const Sidebar: React.FC< React.ComponentPropsWithRef<'div'> & { side?: 'left' | 'right'; variant?: 'sidebar' | 'floating' | 'inset' | 'ghost'; collapsible?: 'offcanvas' | 'icon' | 'none'; } > = ({ side = 'left', variant = 'sidebar', collapsible = 'offcanvas', className, children, ref, ...props }) => { const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === 'none') { return (
{children}
); } if (isMobile) { return ( button]:hidden', { 'bg-background': variant === 'ghost', 'bg-sidebar': variant !== 'ghost', }, )} style={ { '--sidebar-width': SIDEBAR_WIDTH_MOBILE, } as React.CSSProperties } side={side} >
{children}
); } return (
{/* This is what handles the sidebar gap on desktop */}
); }; Sidebar.displayName = 'Sidebar'; const SidebarTrigger: React.FC> = ({ className, onClick, ...props }) => { const context = React.useContext(SidebarContext); if (!context) { return null; } const { toggleSidebar } = context; return ( ); }; SidebarTrigger.displayName = 'SidebarTrigger'; const SidebarRail: React.FC> = ({ className, ...props }) => { const { toggleSidebar } = useSidebar(); return (