diff --git a/packages/ui/src/components/Resizer.tsx b/packages/ui/src/components/Resizer.tsx new file mode 100644 index 0000000..3a1032f --- /dev/null +++ b/packages/ui/src/components/Resizer.tsx @@ -0,0 +1,79 @@ +/** + * Resizer Component + * + * 可拖拽的分割线,用于调整面板宽度 + */ + +import { useState, useCallback, useEffect, useRef } from 'react'; +import { cn } from '../utils/cn.js'; + +interface ResizerProps { + /** 拖拽时的回调,返回拖拽的像素偏移量 */ + onResize: (delta: number) => void; + /** 拖拽结束的回调 */ + onResizeEnd?: () => void; + /** 方向:vertical(左右拖拽)或 horizontal(上下拖拽) */ + direction?: 'vertical' | 'horizontal'; + className?: string; +} + +export function Resizer({ + onResize, + onResizeEnd, + direction = 'vertical', + className, +}: ResizerProps) { + const [isDragging, setIsDragging] = useState(false); + const startPosRef = useRef(0); + + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + setIsDragging(true); + startPosRef.current = direction === 'vertical' ? e.clientX : e.clientY; + }, [direction]); + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!isDragging) return; + + const currentPos = direction === 'vertical' ? e.clientX : e.clientY; + const delta = currentPos - startPosRef.current; + startPosRef.current = currentPos; + onResize(delta); + }, [isDragging, direction, onResize]); + + const handleMouseUp = useCallback(() => { + if (isDragging) { + setIsDragging(false); + onResizeEnd?.(); + } + }, [isDragging, onResizeEnd]); + + useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + // 拖拽时禁止选中文本 + document.body.style.userSelect = 'none'; + document.body.style.cursor = direction === 'vertical' ? 'col-resize' : 'row-resize'; + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.userSelect = ''; + document.body.style.cursor = ''; + }; + }, [isDragging, handleMouseMove, handleMouseUp, direction]); + + return ( +
+ ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 8a1cb75..e7d2323 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -254,6 +254,7 @@ export { FileExplorer } from './components/FileExplorer.js'; export { CodeEditor, getLanguageFromFilename, type EditorTab } from './components/CodeEditor.js'; export { IDE } from './components/IDE.js'; export { StatusBar } from './components/StatusBar.js'; +export { Resizer } from './components/Resizer.js'; // Toast function (re-export from sonner) export { toast } from 'sonner'; diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 0477173..38c59d3 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -17,6 +17,7 @@ import { DiagnosticsPanel, SessionPanel, StatusBar, + Resizer, Toaster, ThemeProvider, listSessions, @@ -38,6 +39,11 @@ export function App() { const [showDiagnostics, setShowDiagnostics] = useState(false); const [showSessions, setShowSessions] = useState(false); const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null); + // IDE 面板宽度(百分比) + const [idePanelWidth, setIdePanelWidth] = useState(() => { + const saved = localStorage.getItem('ai-assistant-ide-width'); + return saved ? parseFloat(saved) : 60; + }); // 初始化:加载会话 useEffect(() => { @@ -97,6 +103,21 @@ export function App() { setSessionTitleUpdate({ sessionId, name }); }, []); + // 处理面板宽度调整 + const handleResize = useCallback((delta: number) => { + setIdePanelWidth((prev) => { + // 计算百分比变化(基于窗口宽度) + const percentDelta = (delta / window.innerWidth) * 100; + // 限制范围:30% - 80% + return Math.min(80, Math.max(30, prev + percentDelta)); + }); + }, []); + + // 保存面板宽度到 localStorage + const handleResizeEnd = useCallback(() => { + localStorage.setItem('ai-assistant-ide-width', String(idePanelWidth)); + }, [idePanelWidth]); + if (isInitializing) { return (