chore: Project cleanup and Canvas UX improvements (Phase 7-9)
## Cleanup (v0.5.0) - Delete 102+ orphaned MCP session temp files - Remove build artifacts (htmlcov, dist, __pycache__) - Archive superseded plan docs (RALPH_LOOP V2/V3, CANVAS V3, etc.) - Move debug/analysis scripts from tests/ to tools/analysis/ - Archive redundant NX journals to archive/nx_journals/ - Archive monolithic PROTOCOL.md to docs/archive/ - Update .gitignore with missing patterns - Clean old study files (optimization_log_old.txt, run_optimization_old.py) ## Canvas UX (Phases 7-9) - Phase 7: Resizable panels with localStorage persistence - Left sidebar: 200-400px, Right panel: 280-600px - New useResizablePanel hook and ResizeHandle component - Phase 8: Enable all palette items - All 8 node types now draggable - Singleton logic for model/solver/algorithm/surrogate - Phase 9: Solver configuration - Add SolverEngine type (nxnastran, mscnastran, python, etc.) - Add NastranSolutionType (SOL101-SOL200) - Engine/solution dropdowns in config panel - Python script path support ## Documentation - Update CHANGELOG.md with recent versions - Update docs/00_INDEX.md - Create examples/README.md - Add docs/plans/CANVAS_UX_IMPROVEMENTS.md
This commit is contained in:
156
atomizer-dashboard/frontend/src/hooks/useResizablePanel.ts
Normal file
156
atomizer-dashboard/frontend/src/hooks/useResizablePanel.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user