"use client"; import type { Dispatch, ReactElement, SetStateAction } from "react"; import React, { useCallback, useEffect, useState, useRef } from "react"; // helpers import { cn } from "@plane/utils"; interface ResizableSidebarProps { showPeek?: boolean; togglePeek: (value?: boolean) => void; isCollapsed?: boolean; width: number; setWidth: Dispatch>; defaultWidth?: number; minWidth?: number; maxWidth?: number; defaultCollapsed?: boolean; peekDuration?: number; toggleCollapsed: (value?: boolean) => void; onWidthChange?: (width: number) => void; onCollapsedChange?: (collapsed: boolean) => void; className?: string; children?: ReactElement; extendedSidebar?: ReactElement; isAnyExtendedSidebarExpanded?: boolean; isAnySidebarDropdownOpen?: boolean; disablePeekTrigger?: boolean; } export function ResizableSidebar({ showPeek = false, togglePeek, peekDuration = 500, isCollapsed = false, toggleCollapsed: toggleCollapsedProp, onCollapsedChange, width, setWidth, onWidthChange, minWidth = 236, maxWidth = 350, className = "", children, extendedSidebar, isAnyExtendedSidebarExpanded = false, isAnySidebarDropdownOpen = false, disablePeekTrigger = false, }: ResizableSidebarProps) { // states const [isResizing, setIsResizing] = useState(false); const [isHoveringTrigger, setIsHoveringTrigger] = useState(false); // refs const peekTimeoutRef = useRef>(); const initialWidthRef = useRef(0); const initialMouseXRef = useRef(0); // handlers const setShowPeek = useCallback( (value: boolean) => { togglePeek(value); }, [togglePeek] ); const handleResize = useCallback( (e: MouseEvent) => { if (!isResizing) return; const deltaX = e.clientX - initialMouseXRef.current; const newWidth = Math.min(Math.max(initialWidthRef.current + deltaX, minWidth), maxWidth); setWidth(newWidth); }, [isResizing, minWidth, maxWidth, setWidth] ); const startResizing = useCallback( (e: React.MouseEvent) => { setIsResizing(true); initialWidthRef.current = width; initialMouseXRef.current = e.clientX; }, [width] ); const stopResizing = useCallback(() => { setIsResizing(false); }, []); const toggleCollapsed = useCallback(() => { toggleCollapsedProp(); setShowPeek(false); setIsHoveringTrigger(false); if (peekTimeoutRef.current) { clearTimeout(peekTimeoutRef.current); } }, [toggleCollapsedProp, setShowPeek]); const handleTriggerEnter = useCallback(() => { if (isCollapsed) { setIsHoveringTrigger(true); setShowPeek(true); if (peekTimeoutRef.current) { clearTimeout(peekTimeoutRef.current); } } }, [isCollapsed, setShowPeek]); const handleTriggerLeave = useCallback(() => { if (isCollapsed && !isAnyExtendedSidebarExpanded) { setIsHoveringTrigger(false); peekTimeoutRef.current = setTimeout(() => { setShowPeek(false); }, peekDuration); } }, [isCollapsed, peekDuration, setShowPeek, isAnyExtendedSidebarExpanded]); const handlePeekEnter = useCallback(() => { if (isCollapsed && showPeek) { if (peekTimeoutRef.current) { clearTimeout(peekTimeoutRef.current); } } }, [isCollapsed, showPeek]); const handlePeekLeave = useCallback(() => { if (isCollapsed && !isAnyExtendedSidebarExpanded && !isAnySidebarDropdownOpen) { peekTimeoutRef.current = setTimeout(() => { setShowPeek(false); }, peekDuration); } }, [isCollapsed, peekDuration, setShowPeek, isAnyExtendedSidebarExpanded, isAnySidebarDropdownOpen]); // Set up event listeners for resizing useEffect(() => { if (isResizing) { document.addEventListener("mousemove", handleResize); document.addEventListener("mouseup", stopResizing); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; } return () => { document.removeEventListener("mousemove", handleResize); document.removeEventListener("mouseup", stopResizing); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; }, [isResizing, handleResize, stopResizing]); // Clean up timeout on unmount useEffect( () => () => { if (peekTimeoutRef.current) { clearTimeout(peekTimeoutRef.current); } }, [] ); useEffect(() => { if (!isAnySidebarDropdownOpen && isCollapsed && isHoveringTrigger) { handlePeekLeave(); } }, [isAnySidebarDropdownOpen]); useEffect(() => { if (!isAnyExtendedSidebarExpanded && isCollapsed && isHoveringTrigger) { handlePeekLeave(); } }, [isAnyExtendedSidebarExpanded]); // Reset peek when sidebar is expanded useEffect(() => { if (!isCollapsed) { setShowPeek(false); setIsHoveringTrigger(false); if (peekTimeoutRef.current) { clearTimeout(peekTimeoutRef.current); } } }, [isCollapsed, setShowPeek]); // Call external handlers when state changes useEffect(() => { onWidthChange?.(width); }, [width, onWidthChange]); useEffect(() => { onCollapsedChange?.(isCollapsed); }, [isCollapsed, onCollapsedChange]); return ( <> {/* Main Sidebar */}
{/* Peek Trigger Area */} {isCollapsed && !disablePeekTrigger && (
)} {/* Peek View */}
{/* Extended Sidebar */} {extendedSidebar && extendedSidebar} ); }