Files
Atomizer/atomizer-dashboard/frontend/src/hooks/useResizablePanel.ts

157 lines
4.4 KiB
TypeScript
Raw Normal View History

/**
* useResizablePanel - Hook for creating resizable panels with persistence
*
* Features:
* - Drag to resize
* - Min/max constraints
* - localStorage persistence
* - Double-click to reset to default
*/
import { useState, useCallback, useEffect, useRef } from 'react';
export interface ResizablePanelConfig {
/** Unique key for localStorage persistence */
storageKey: string;
/** Default width in pixels */
defaultWidth: number;
/** Minimum width in pixels */
minWidth: number;
/** Maximum width in pixels */
maxWidth: number;
/** Side of the panel ('left' or 'right') - affects resize direction */
side: 'left' | 'right';
}
export interface ResizablePanelState {
/** Current width in pixels */
width: number;
/** Whether user is currently dragging */
isDragging: boolean;
/** Start drag handler - attach to resize handle mousedown */
startDrag: (e: React.MouseEvent) => void;
/** Reset to default width */
resetWidth: () => void;
/** Set width programmatically */
setWidth: (width: number) => void;
}
const STORAGE_PREFIX = 'atomizer-panel-';
function getStoredWidth(key: string, defaultWidth: number): number {
if (typeof window === 'undefined') return defaultWidth;
try {
const stored = localStorage.getItem(STORAGE_PREFIX + key);
if (stored) {
const parsed = parseInt(stored, 10);
if (!isNaN(parsed)) return parsed;
}
} catch {
// localStorage not available
}
return defaultWidth;
}
function storeWidth(key: string, width: number): void {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(STORAGE_PREFIX + key, String(width));
} catch {
// localStorage not available
}
}
export function useResizablePanel(config: ResizablePanelConfig): ResizablePanelState {
const { storageKey, defaultWidth, minWidth, maxWidth, side } = config;
// Initialize from localStorage
const [width, setWidthState] = useState(() => {
const stored = getStoredWidth(storageKey, defaultWidth);
return Math.max(minWidth, Math.min(maxWidth, stored));
});
const [isDragging, setIsDragging] = useState(false);
// Track initial position for drag calculation
const dragStartRef = useRef<{ x: number; width: number } | null>(null);
// Clamp width within bounds
const clampWidth = useCallback((w: number) => {
return Math.max(minWidth, Math.min(maxWidth, w));
}, [minWidth, maxWidth]);
// Set width with clamping and persistence
const setWidth = useCallback((newWidth: number) => {
const clamped = clampWidth(newWidth);
setWidthState(clamped);
storeWidth(storageKey, clamped);
}, [clampWidth, storageKey]);
// Reset to default
const resetWidth = useCallback(() => {
setWidth(defaultWidth);
}, [defaultWidth, setWidth]);
// Start drag handler
const startDrag = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
dragStartRef.current = { x: e.clientX, width };
}, [width]);
// Handle mouse move during drag
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
if (!dragStartRef.current) return;
const delta = e.clientX - dragStartRef.current.x;
// For left panels, positive delta increases width
// For right panels, negative delta increases width
const newWidth = side === 'left'
? dragStartRef.current.width + delta
: dragStartRef.current.width - delta;
setWidthState(clampWidth(newWidth));
};
const handleMouseUp = () => {
if (dragStartRef.current) {
// Persist the final width
storeWidth(storageKey, width);
}
setIsDragging(false);
dragStartRef.current = null;
};
// Add listeners to document for smooth dragging
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
// Change cursor globally during drag
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isDragging, side, clampWidth, storageKey, width]);
return {
width,
isDragging,
startDrag,
resetWidth,
setWidth,
};
}
export default useResizablePanel;