Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
215 lines
7.7 KiB
TypeScript
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,
|
|
};
|
|
}
|