"use client"; import { Tooltip } from "@base-ui/react/tooltip"; import { PanelLeft } from "lucide-react"; import { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState, type ComponentProps, type ForwardedRef, } from "react"; import { useSidebarViewport } from "@/hooks/use-sidebar-viewport"; import { defaultSidebarOpen, shouldHandleSidebarShortcut, type SidebarViewport, } from "@/lib/sidebar-state"; import { cn } from "@/lib/utils"; export type SidebarState = "expanded" | "collapsed"; export type SidebarContextValue = { readonly state: SidebarState; readonly open: boolean; readonly setOpen: (open: boolean) => void; readonly openMobile: boolean; readonly setOpenMobile: (open: boolean) => void; readonly viewport: SidebarViewport; readonly ready: boolean; readonly isMobile: boolean; readonly toggleSidebar: () => void; }; type SidebarProviderContextValue = SidebarContextValue & { readonly setInsetTrigger: (element: HTMLButtonElement | null) => void; readonly setSidebarSurface: (element: HTMLElement | null) => void; }; export type SidebarProviderProps = ComponentProps<"div"> & { readonly defaultOpen?: boolean; readonly open?: boolean; readonly onOpenChange?: (open: boolean) => void; readonly onMobileOpenChange?: (open: boolean) => void; readonly escapeBlocked?: boolean; }; export type SidebarTriggerProps = ComponentProps<"button"> & { readonly placement?: "inset" | "sidebar"; }; type SidebarMenuButtonProps = ComponentProps<"button"> & { readonly isActive?: boolean; readonly tooltip?: string; }; const SidebarContext = createContext(null); function useSidebarContext(): SidebarProviderContextValue { const context = useContext(SidebarContext); if (context === null) throw new Error("Sidebar components must be used within SidebarProvider"); return context; } export function useSidebar(): SidebarContextValue { return useSidebarContext(); } export function SidebarProvider({ className, defaultOpen = true, open: controlledOpen, onOpenChange, onMobileOpenChange, escapeBlocked = false, ...props }: SidebarProviderProps) { const { viewport, ready } = useSidebarViewport(); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); const [openMobile, setOpenMobileState] = useState(false); const openMobileRef = useRef(false); const userChangedDesktopState = useRef(false); const insetTriggerRef = useRef(null); const sidebarSurfaceRef = useRef(null); const wasMobileOpen = useRef(false); const isControlled = controlledOpen !== undefined; const open = controlledOpen ?? uncontrolledOpen; const isMobile = viewport === "mobile"; const commitOpen = useCallback((nextOpen: boolean) => { userChangedDesktopState.current = true; if (!isControlled) setUncontrolledOpen(nextOpen); onOpenChange?.(nextOpen); }, [isControlled, onOpenChange]); const setOpen = useCallback((nextOpen: boolean) => { commitOpen(nextOpen); }, [commitOpen]); const setOpenMobile = useCallback((nextOpen: boolean) => { if (openMobileRef.current === nextOpen) return; openMobileRef.current = nextOpen; setOpenMobileState(nextOpen); onMobileOpenChange?.(nextOpen); }, [onMobileOpenChange]); useEffect(() => { if (ready && !isControlled && !userChangedDesktopState.current) { setUncontrolledOpen(defaultSidebarOpen(viewport)); } }, [isControlled, ready, viewport]); useEffect(() => { if (viewport === "mobile" || !openMobile) return; const closeMobileDrawer = window.requestAnimationFrame(() => setOpenMobile(false)); return () => window.cancelAnimationFrame(closeMobileDrawer); }, [openMobile, setOpenMobile, viewport]); useEffect(() => { const handleKeydown = (event: KeyboardEvent) => { if (shouldHandleSidebarShortcut(event)) { event.preventDefault(); if (isMobile) setOpenMobile(!openMobile); else commitOpen(!open); } if (event.key === "Escape" && isMobile && openMobile && !escapeBlocked) { event.preventDefault(); setOpenMobile(false); } }; window.addEventListener("keydown", handleKeydown); return () => window.removeEventListener("keydown", handleKeydown); }, [commitOpen, escapeBlocked, isMobile, open, openMobile, setOpenMobile]); useEffect(() => { if (openMobile && !wasMobileOpen.current) { const focusDrawer = window.requestAnimationFrame(() => sidebarSurfaceRef.current?.focus()); wasMobileOpen.current = true; return () => window.cancelAnimationFrame(focusDrawer); } if (!openMobile && wasMobileOpen.current) insetTriggerRef.current?.focus(); wasMobileOpen.current = openMobile; }, [openMobile]); const toggleSidebar = useCallback(() => { if (isMobile) setOpenMobile(!openMobile); else setOpen(!open); }, [isMobile, open, openMobile, setOpen, setOpenMobile]); const context: SidebarProviderContextValue = { state: open ? "expanded" : "collapsed", open, setOpen, openMobile, setOpenMobile, viewport, ready, isMobile, toggleSidebar, setInsetTrigger: (element) => { insetTriggerRef.current = element; }, setSidebarSurface: (element) => { sidebarSurfaceRef.current = element; }, }; return (
); } export function Sidebar({ id, className, ...props }: ComponentProps<"aside">) { const { isMobile, open, openMobile, setOpenMobile, setSidebarSurface } = useSidebarContext(); const sidebar =