"use client" import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "../../lib/utils.js" import { Portal as TooltipPortal } from "@radix-ui/react-tooltip" import { Slot } from "@radix-ui/react-slot" import { Button } from "./button.js" import { Input } from "./input.js" import { Separator } from "./separator.js" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "./sheet.js" import { Skeleton } from "./skeleton.js" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./tooltip.js" function useIsMobile() { const [isMobile, setIsMobile] = React.useState(false) React.useEffect(() => { const query = window.matchMedia("(max-width: 767px)") const update = () => setIsMobile(query.matches) update() query.addEventListener("change", update) return () => query.removeEventListener("change", update) }, []) return isMobile } 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 = "3.5rem" const SIDEBAR_KEYBOARD_SHORTCUT = "b" type SidebarContextProps = { state: "expanded" | "collapsed" open: boolean setOpen: React.Dispatch> openMobile: boolean setOpenMobile: React.Dispatch> isMobile: boolean toggleSidebar: () => void /** Compatibility alias for the original EnoughUI context. */ expanded: boolean sidebarId: string mobileStyle: React.CSSProperties mobileReturnFocusRef: React.RefObject dir?: string } 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, expanded, onExpandedChange, persistState = true, cookieName = SIDEBAR_COOKIE_NAME, cookieMaxAge = SIDEBAR_COOKIE_MAX_AGE, className, style, children, dir, id, ...props }: React.ComponentProps<"div"> & { defaultOpen?: boolean open?: boolean onOpenChange?: (open: boolean) => void /** @deprecated Use open instead. */ expanded?: boolean /** @deprecated Use onOpenChange instead. */ onExpandedChange?: (expanded: boolean) => void /** Set false to disable sidebar state cookie writes. */ persistState?: boolean cookieName?: string cookieMaxAge?: number }) { const isMobile = useIsMobile() const sidebarId = React.useId() const mobileReturnFocusRef = React.useRef(null) 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 controlledOpen = openProp ?? expanded const open = controlledOpen ?? _open const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { const openState = typeof value === "function" ? value(open) : value if (controlledOpen === undefined) _setOpen(openState) setOpenProp?.(openState) onExpandedChange?.(openState) if (persistState) { document.cookie = `${encodeURIComponent(cookieName)}=${openState}; path=/; max-age=${cookieMaxAge}; samesite=lax` } }, [setOpenProp, onExpandedChange, controlledOpen, open, persistState, cookieName, cookieMaxAge] ) // Helper to toggle the sidebar. const toggleSidebar = React.useCallback(() => { if (isMobile) { if (!openMobile && !mobileReturnFocusRef.current && document.activeElement instanceof HTMLElement) { mobileReturnFocusRef.current = document.activeElement } setOpenMobile((value) => !value) } else { setOpen((value) => !value) } }, [isMobile, setOpen, openMobile]) // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { const target = event.target const editing = target instanceof HTMLElement && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)) if ( !editing && !event.repeat && !event.altKey && event.key.toLowerCase() === 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, expanded: open, sidebarId, mobileStyle: style ?? {}, mobileReturnFocusRef, dir, }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, sidebarId, style, dir] ) return (
{children}
) } function Sidebar({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, dir, id, ...props }: React.ComponentProps<"div"> & { side?: "left" | "right" variant?: "sidebar" | "floating" | "inset" collapsible?: "offcanvas" | "icon" | "none" }) { const { isMobile, state, openMobile, setOpenMobile, sidebarId, mobileStyle, mobileReturnFocusRef, dir: providerDir } = useSidebar() if (collapsible === "none") { return (
{children}
) } if (isMobile) { return ( { event.preventDefault() mobileReturnFocusRef.current?.focus() mobileReturnFocusRef.current = null }} > Sidebar Navigate the application using the mobile sidebar.
{children}
) } return (
{/* This is what handles the sidebar gap on desktop */}
) } function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) { const { toggleSidebar, open, openMobile, isMobile, mobileReturnFocusRef } = useSidebar() return ( ) } function SidebarRail({ className, onClick, ...props }: React.ComponentProps<"button">) { const { toggleSidebar } = useSidebar() return (