/* eslint-disable max-lines */ "use client"; import {PanelLeft} from "lucide-react"; import * as React from "react"; import {createPortal} from "react-dom"; import {Button} from "@/components/ui/button"; import {Input} from "@/components/ui/input"; import {Separator} from "@/components/ui/separator"; import {Skeleton} from "@/components/ui/skeleton"; import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; import {useIsMobile} from "@/hooks/useIsMobile"; import {cn} from "@/lib/utilities"; import styles from "./sidebar.module.css"; 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 SidebarDataAttributes = Record<`data-${string}`, string | boolean | undefined>; type SidebarCloneableDivProps = React.ComponentProps<"div"> & SidebarDataAttributes & {ref?: React.Ref}; type SidebarCloneableButtonProps = React.ComponentProps<"button"> & SidebarDataAttributes & {ref?: React.Ref}; type SidebarCloneableAnchorProps = React.ComponentProps<"a"> & SidebarDataAttributes & {ref?: React.Ref}; 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); /** * Returns the active sidebar context and enforces provider usage. * * @remarks * Must be called from within {@link SidebarProvider}. Exposes desktop and mobile * open state along with the shared toggle helper used by sidebar primitives. * * @example * ```tsx * const {open, toggleSidebar} = useSidebar(); * ``` * * @see {@link https://react.dev/reference/react/useContext | React useContext Docs} */ function useSidebar(): SidebarContextProps { const context = React.useContext(SidebarContext); if (!context) { throw new Error("useSidebar must be used within a SidebarProvider."); } return context; } /** * Props for the sidebar provider. */ interface SidebarProviderProps extends React.ComponentProps<"div"> { /** * Initial uncontrolled open state for desktop layouts. * @default true */ defaultOpen?: boolean; /** * Controlled open state for desktop layouts. * @default undefined */ open?: boolean; /** * Callback invoked when the desktop open state changes. * @default undefined */ onOpenChange?: (open: boolean) => void; } /** * Provides shared sidebar state, keyboard shortcuts, and responsive behavior. * * @remarks * - Renders a `
` element * - Built on shared React context and tooltip primitives * - Persists desktop collapse state to a cookie for cross-page continuity * * @example * ```tsx * * * * ``` * * @see {@link https://react.dev/reference/react/useContext | React Context Docs} */ const SidebarProvider = React.forwardRef( ({defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props}, ref) => { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false); const [internalOpen, setInternalOpen] = React.useState(defaultOpen); const open = openProp ?? internalOpen; const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { const nextValue = typeof value === "function" ? value(open) : value; if (setOpenProp) { setOpenProp(nextValue); } else { setInternalOpen(nextValue); } // eslint-disable-next-line unicorn/no-document-cookie -- persistent sidebar state matches V1 behavior document.cookie = `${SIDEBAR_COOKIE_NAME}=${nextValue}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, [open, setOpenProp], ); const toggleSidebar = React.useCallback(() => { if (isMobile) { setOpenMobile((currentOpen) => !currentOpen); return; } setOpen((currentOpen) => !currentOpen); }, [isMobile, setOpen]); React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { event.preventDefault(); toggleSidebar(); } }; globalThis.window.addEventListener("keydown", handleKeyDown); return () => { globalThis.window.removeEventListener("keydown", handleKeyDown); }; }, [toggleSidebar]); const state = open ? "expanded" : "collapsed"; const contextValue = React.useMemo( () => ({ isMobile, open, openMobile, setOpen, setOpenMobile, state, toggleSidebar, }), [isMobile, open, openMobile, setOpen, state, toggleSidebar], ); return (
{children}
); }, ); SidebarProvider.displayName = "SidebarProvider"; /** * Props for the root sidebar panel. */ type SidebarProps = React.ComponentProps<"div"> & { /** * Edge of the screen where the sidebar is rendered. * @default "left" */ side?: "left" | "right"; /** * Visual presentation style used for desktop rendering. * @default "sidebar" */ variant?: "sidebar" | "floating" | "inset"; /** * Desktop collapse behavior for the sidebar. * @default "offcanvas" */ collapsible?: "offcanvas" | "icon" | "none"; }; /** * Renders the responsive sidebar panel for desktop and mobile layouts. * * @remarks * - Renders a `
` element on desktop and a dialog portal on mobile * - Built on the shared sidebar context * * @example * ```tsx * * * * ``` * * @see {@link https://developer.mozilla.org/docs/Web/Accessibility/ARIA/Roles/dialog_role | ARIA Dialog Role} */ const Sidebar = React.forwardRef( ({side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props}, ref) => { const {isMobile, openMobile, setOpenMobile, state} = useSidebar(); if (collapsible === "none") { return (
{children}
); } if (isMobile) { return ( {children} ); } return (
{children}
); }, ); Sidebar.displayName = "Sidebar"; type MobileSidebarPortalProps = React.ComponentProps<"div"> & { open: boolean; side: "left" | "right"; onOpenChange: (open: boolean) => void; }; function MobileSidebarPortal({ open, side, onOpenChange, className, children, ...props }: Readonly): React.ReactPortal | null { React.useEffect(() => { if (!open) { return; } const previousOverflow = document.body.style.overflow; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { onOpenChange(false); } }; document.body.style.overflow = "hidden"; globalThis.window.addEventListener("keydown", handleKeyDown); return () => { document.body.style.overflow = previousOverflow; globalThis.window.removeEventListener("keydown", handleKeyDown); }; }, [onOpenChange, open]); if (!open || typeof document === "undefined") { return null; } return createPortal(
, document.body, ); } /** * Renders the primary button used to toggle the sidebar. * * @remarks * - Renders the shared `Button` component * - Built on the shared sidebar context * * @example * ```tsx * * ``` * * @see {@link https://developer.mozilla.org/docs/Web/HTML/Element/button | HTML button element} */ const SidebarTrigger = React.forwardRef, React.ComponentProps>( ({className, onClick, ...props}, ref) => { const {toggleSidebar} = useSidebar(); return ( ); }, ); SidebarTrigger.displayName = "SidebarTrigger"; /** * Renders a slim rail button used to toggle the sidebar collapsed state. * * @remarks * - Renders a `