godcrm/hooks/usePanelResize.ts
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

215 lines
7.7 KiB
TypeScript

/**
* ADR-097: Panel resize handlers
* Extracted from AIChatPanel.tsx (lines 1903-2054)
*
* Provides vertical/horizontal/sidebar resize via mouse and touch,
* plus panel mode toggling.
*/
import { useCallback } from 'react';
export type PanelMode = 'collapsed' | 'expanded' | 'default' | 'fullscreen';
export type PanelTab = 'none' | 'contacts' | 'ai-agents' | 'tasks' | 'settings' | 'inbox';
export interface UsePanelResizeParams {
panelHeight: number | 'auto';
panelWidth: number;
sidebarWidth: number;
panelMode: PanelMode;
activePanel: PanelTab;
setPanelHeight: (v: number | 'auto') => void;
setPanelWidth: (v: number) => void;
setSidebarWidth: (v: number) => void;
setPanelMode: (v: PanelMode | ((prev: PanelMode) => PanelMode)) => void;
setIsResizing: (v: boolean) => void;
setIsResizingWidth: (v: boolean) => void;
setIsResizingSidebar: (v: boolean) => void;
setActivePanel: (v: PanelTab) => void;
}
export interface UsePanelResizeReturn {
handleResizeStart: (e: React.MouseEvent) => void;
handleTouchResizeStart: (e: React.TouchEvent) => void;
handleWidthResizeStart: (e: React.MouseEvent) => void;
handleSidebarResizeStart: (e: React.MouseEvent) => void;
togglePanelMode: () => void;
togglePanel: (panel: PanelTab) => void;
}
export function usePanelResize({
panelHeight,
panelWidth,
sidebarWidth,
panelMode,
activePanel,
setPanelHeight,
setPanelWidth,
setSidebarWidth,
setPanelMode,
setIsResizing,
setIsResizingWidth,
setIsResizingSidebar,
setActivePanel,
}: UsePanelResizeParams): UsePanelResizeReturn {
// Resize handler (mouse) - with snap to modes
// Logic: min 400px panel, max containerHeight - 400px (leaves 400px for chat)
const handleResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsResizing(true);
const startY = e.clientY;
const container = (e.target as HTMLElement).closest('[data-panel-container]');
const containerHeight = container?.parentElement?.clientHeight || 600;
const startHeight = typeof panelHeight === 'number' ? panelHeight : containerHeight;
const minHeight = 400;
const maxHeight = containerHeight - 400; // Always leave 400px for chat
const handleMouseMove = (e: MouseEvent) => {
const delta = e.clientY - startY;
const newHeight = startHeight + delta;
// Snap logic:
// - Below 400px -> snap to collapsed (400px fixed)
// - Above maxHeight -> snap to max (leaves 400px for chat)
// - Between -> free resize
if (newHeight <= minHeight) {
setPanelHeight(minHeight);
setPanelMode('collapsed');
} else if (newHeight >= maxHeight) {
setPanelHeight(maxHeight);
setPanelMode('default');
} else {
// Free resize zone - expanded mode with specific height
setPanelHeight(newHeight);
setPanelMode('expanded');
}
};
const handleMouseUp = () => {
setIsResizing(false);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, [panelHeight, setPanelHeight, setPanelMode, setIsResizing]);
// Touch resize handler for mobile - same logic
// Note: Using touch-action: none on the element instead of preventDefault to avoid passive listener warnings
const handleTouchResizeStart = useCallback((e: React.TouchEvent) => {
setIsResizing(true);
const startY = e.touches[0].clientY;
const container = (e.target as HTMLElement).closest('[data-panel-container]');
const containerHeight = container?.parentElement?.clientHeight || 600;
const startHeight = typeof panelHeight === 'number' ? panelHeight : containerHeight;
const minHeight = 400;
const maxHeight = containerHeight - 400; // Always leave 400px for chat
const handleTouchMove = (e: TouchEvent) => {
const delta = e.touches[0].clientY - startY;
const newHeight = startHeight + delta;
if (newHeight <= minHeight) {
setPanelHeight(minHeight);
setPanelMode('collapsed');
} else if (newHeight >= maxHeight) {
setPanelHeight(maxHeight);
setPanelMode('default');
} else {
setPanelHeight(newHeight);
setPanelMode('expanded');
}
};
const handleTouchEnd = () => {
setIsResizing(false);
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', handleTouchEnd);
};
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd);
}, [panelHeight, setPanelHeight, setPanelMode, setIsResizing]);
// Horizontal resize handler (for expanding chat left)
const handleWidthResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsResizingWidth(true);
const startX = e.clientX;
const startWidth = panelWidth;
const handleMouseMove = (e: MouseEvent) => {
const delta = startX - e.clientX; // moving left increases width
const newWidth = Math.max(320, Math.min(1170, startWidth + delta));
setPanelWidth(newWidth);
};
const handleMouseUp = () => {
setIsResizingWidth(false);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, [panelWidth, setPanelWidth, setIsResizingWidth]);
// Sidebar resize handler (between chat and sidebar)
const handleSidebarResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsResizingSidebar(true);
const startX = e.clientX;
const startWidth = sidebarWidth;
const handleMouseMove = (e: MouseEvent) => {
const delta = startX - e.clientX; // moving left increases sidebar width
const newWidth = Math.max(300, Math.min(500, startWidth + delta));
setSidebarWidth(newWidth);
};
const handleMouseUp = () => {
setIsResizingSidebar(false);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}, [sidebarWidth, setSidebarWidth, setIsResizingSidebar]);
// Panel mode toggle - cycles through 4 modes
// collapsed -> expanded -> default -> fullscreen -> collapsed
// collapsed = minimal bar, expanded = shows 400px chat, default = fills to input, fullscreen = covers everything
const togglePanelMode = useCallback(() => {
setPanelMode((prev: PanelMode) => {
if (prev === 'collapsed') return 'expanded';
if (prev === 'expanded') return 'default';
if (prev === 'default') return 'fullscreen';
return 'collapsed'; // fullscreen -> collapsed
});
setPanelHeight('auto');
}, [setPanelMode, setPanelHeight]);
// Toggle panel -- reset panelMode when closing to prevent stale fullscreen hiding input
const togglePanel = (panel: PanelTab) => {
if (activePanel === panel) {
setActivePanel('none');
// Bug fix: Reset panelMode when closing panel -- otherwise panelMode stays 'fullscreen'
// and the input area remains hidden (line ~4402: panelMode !== 'fullscreen')
if (panelMode === 'fullscreen' || panelMode === 'expanded') {
setPanelMode('collapsed');
}
} else {
setActivePanel(panel);
}
};
return {
handleResizeStart,
handleTouchResizeStart,
handleWidthResizeStart,
handleSidebarResizeStart,
togglePanelMode,
togglePanel,
};
}