// This component is a work in ProgressEvent. Not ready for production. import "../styles/allotment.css"; import { Allotment, LayoutPriority } from "allotment"; import * as React from "react"; import { customColors } from "@sparkle/lib/colors"; import { cn } from "@sparkle/lib/utils"; /** * Props for the SidebarLayout component */ export interface SidebarLayoutProps { /** The sidebar content to display */ sidebar: React.ReactNode; /** The main content area */ content: React.ReactNode; /** Default sidebar width in pixels (default: 280) */ defaultSidebarWidth?: number; /** Minimum sidebar width in pixels (default: 200) */ minSidebarWidth?: number; /** Maximum sidebar width in pixels (default: 400) */ maxSidebarWidth?: number; /** Whether the sidebar can be collapsed (default: true) */ collapsible?: boolean; /** Callback function called when sidebar is toggled */ onSidebarToggle?: (collapsed: boolean) => void; /** Additional className for the container */ className?: string; /** Additional className for the sidebar pane */ sidebarClassName?: string; /** Additional className for the content pane */ contentClassName?: string; } /** * Ref methods exposed by SidebarLayout */ export interface SidebarLayoutRef { /** Toggle the sidebar collapsed state */ toggle: () => void; /** Collapse the sidebar */ collapse: () => void; /** Expand the sidebar */ expand: () => void; } /** * SidebarLayout component provides a resizable sidebar layout using Allotment. * Supports pixel-based sizing, toggle functionality, and hover reveal when collapsed. */ export const SidebarLayout = React.forwardRef< SidebarLayoutRef, SidebarLayoutProps >(function SidebarLayout( { sidebar, content, defaultSidebarWidth = 280, minSidebarWidth = 200, maxSidebarWidth = 400, collapsible = true, onSidebarToggle, className, sidebarClassName, contentClassName, }, ref ) { const allotmentRef = React.useRef>(null); const [isSidebarCollapsed, setIsSidebarCollapsed] = React.useState(false); const [isHovering, setIsHovering] = React.useState(false); const [sidebarWidth, setSidebarWidth] = React.useState(defaultSidebarWidth); const isTogglingRef = React.useRef(false); // Toggle sidebar function const toggleSidebar = React.useCallback(() => { if (allotmentRef.current) { isTogglingRef.current = true; if (isSidebarCollapsed) { // Expand to last stored width - use requestAnimationFrame to ensure size is set correctly requestAnimationFrame(() => { if (allotmentRef.current) { allotmentRef.current.resize([sidebarWidth]); } }); setIsSidebarCollapsed(false); setIsHovering(false); onSidebarToggle?.(false); } else { // Collapse to 0 allotmentRef.current.resize([0]); setIsSidebarCollapsed(true); setIsHovering(false); onSidebarToggle?.(true); } // Reset toggle flag after animation completes setTimeout(() => { isTogglingRef.current = false; }, 300); } }, [isSidebarCollapsed, sidebarWidth, onSidebarToggle]); // Handle hover reveal when collapsed const handleLeftEdgeHover = React.useCallback(() => { if (isSidebarCollapsed && collapsible) { // Set hovering state - this will trigger overlay render setIsHovering(true); } }, [isSidebarCollapsed, collapsible]); const handleSidebarMouseLeave = React.useCallback(() => { if (isSidebarCollapsed && isHovering && collapsible) { // Hide overlay but keep collapsed state unchanged setIsHovering(false); } }, [isSidebarCollapsed, isHovering, collapsible]); // Handle size changes from Allotment const handleChange = React.useCallback( (sizes: number[]) => { const sidebarSize = sizes[0] ?? 0; const wasCollapsed = isSidebarCollapsed; const nowCollapsed = sidebarSize === 0; // Track sidebar width whenever it changes (when not toggling and not collapsed) if (!isTogglingRef.current && !wasCollapsed && sidebarSize > 0) { setSidebarWidth(sidebarSize); } // Only update collapsed state if it's from toggle button // Hover reveal should not change the collapsed state if (wasCollapsed !== nowCollapsed && isTogglingRef.current) { setIsSidebarCollapsed(nowCollapsed); if (!nowCollapsed) { setIsHovering(false); } onSidebarToggle?.(nowCollapsed); } }, [isSidebarCollapsed, onSidebarToggle] ); // Expose methods via ref React.useImperativeHandle( ref, () => ({ toggle: toggleSidebar, collapse: () => { if (allotmentRef.current && !isSidebarCollapsed) { isTogglingRef.current = true; allotmentRef.current.resize([0]); setIsSidebarCollapsed(true); setIsHovering(false); onSidebarToggle?.(true); setTimeout(() => { isTogglingRef.current = false; }, 300); } }, expand: () => { if (allotmentRef.current && isSidebarCollapsed) { isTogglingRef.current = true; requestAnimationFrame(() => { if (allotmentRef.current) { allotmentRef.current.resize([sidebarWidth]); } }); setIsSidebarCollapsed(false); setIsHovering(false); onSidebarToggle?.(false); setTimeout(() => { isTogglingRef.current = false; }, 300); } }, }), [toggleSidebar, isSidebarCollapsed, sidebarWidth, onSidebarToggle] ); return (
{/* Allotment CSS variables for resize border customization */} {/* Hover zone when collapsed */} {isSidebarCollapsed && collapsible && ( ); });