Skip to content

Commit 78866ef

Browse files
committed
Add resizable terminal panels
- Implement drag-based resizing for 2-terminal and 3-terminal layouts - Add drag handles between panels with visual feedback - Maintain minimum and maximum size constraints - Fix TypeScript errors and boolean arithmetic issues
1 parent ae73c70 commit 78866ef

File tree

1 file changed

+120
-20
lines changed

1 file changed

+120
-20
lines changed

src/components/preview_panel/Console.tsx

Lines changed: 120 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,94 @@
11
import { appOutputAtom, frontendTerminalOutputAtom, backendTerminalOutputAtom, selectedAppIdAtom } from "@/atoms/appAtoms";
22
import { useAtomValue } from "jotai";
33
import { useLoadApp } from "@/hooks/useLoadApp";
4+
import { useState, useRef, useCallback, useEffect } from "react";
5+
import * as React from "react";
46

5-
// Console component with side-by-side terminal support
7+
// Console component with side-by-side terminal support and resizable panels
68
export const Console = () => {
79
const appOutput = useAtomValue(appOutputAtom);
810
const frontendTerminalOutput = useAtomValue(frontendTerminalOutputAtom);
911
const backendTerminalOutput = useAtomValue(backendTerminalOutputAtom);
1012
const selectedAppId = useAtomValue(selectedAppIdAtom);
1113
const { app } = useLoadApp(selectedAppId);
1214

15+
// State for panel sizes (percentages)
16+
const [panelSizes, setPanelSizes] = useState({
17+
left: 50,
18+
right: 50,
19+
left2: 33.33,
20+
middle: 33.33,
21+
right2: 33.34,
22+
});
23+
24+
// Refs for drag handles
25+
const dragHandleRef = useRef<HTMLDivElement>(null);
26+
const dragHandle2Ref = useRef<HTMLDivElement>(null);
27+
const dragHandle3Ref = useRef<HTMLDivElement>(null);
28+
29+
// Drag state
30+
const [isDragging, setIsDragging] = useState<string | null>(null);
31+
const [startX, setStartX] = useState(0);
32+
const [startSizes, setStartSizes] = useState(panelSizes);
33+
34+
const handleMouseDown = useCallback((handleId: string, event: React.MouseEvent) => {
35+
setIsDragging(handleId);
36+
setStartX(event.clientX);
37+
setStartSizes({ ...panelSizes });
38+
event.preventDefault();
39+
}, [panelSizes]);
40+
41+
const handleMouseMove = useCallback((event: MouseEvent) => {
42+
if (!isDragging) return;
43+
44+
const container = document.querySelector('[data-terminal-container]');
45+
if (!container) return;
46+
47+
const rect = container.getBoundingClientRect();
48+
const deltaX = event.clientX - startX;
49+
const containerWidth = rect.width;
50+
51+
if (isDragging === 'left-right') {
52+
// Two panel resize
53+
const deltaPercent = (deltaX / containerWidth) * 100;
54+
const newLeft = Math.max(20, Math.min(80, startSizes.left + deltaPercent));
55+
const newRight = 100 - newLeft;
56+
setPanelSizes(prev => ({ ...prev, left: newLeft, right: newRight }));
57+
} else if (isDragging === 'left-middle') {
58+
// Three panel resize (left-middle)
59+
const deltaPercent = (deltaX / containerWidth) * 100;
60+
const newLeft2 = Math.max(15, Math.min(50, startSizes.left2 + deltaPercent));
61+
const totalMiddleRight = 100 - newLeft2;
62+
const newMiddle = totalMiddleRight * (startSizes.middle / (startSizes.middle + startSizes.right2));
63+
const newRight2 = totalMiddleRight - newMiddle;
64+
setPanelSizes(prev => ({ ...prev, left2: newLeft2, middle: newMiddle, right2: newRight2 }));
65+
} else if (isDragging === 'middle-right') {
66+
// Three panel resize (middle-right)
67+
const deltaPercent = (deltaX / containerWidth) * 100;
68+
const newRight2 = Math.max(15, Math.min(50, startSizes.right2 - deltaPercent));
69+
const totalLeftMiddle = 100 - newRight2;
70+
const newMiddle = totalLeftMiddle * (startSizes.middle / (startSizes.left2 + startSizes.middle));
71+
const newLeft2 = totalLeftMiddle - newMiddle;
72+
setPanelSizes(prev => ({ ...prev, left2: newLeft2, middle: newMiddle, right2: newRight2 }));
73+
}
74+
}, [isDragging, startX, startSizes]);
75+
76+
const handleMouseUp = useCallback(() => {
77+
setIsDragging(null);
78+
}, []);
79+
80+
// Add global mouse event listeners
81+
useEffect(() => {
82+
if (isDragging) {
83+
document.addEventListener('mousemove', handleMouseMove);
84+
document.addEventListener('mouseup', handleMouseUp);
85+
return () => {
86+
document.removeEventListener('mousemove', handleMouseMove);
87+
document.removeEventListener('mouseup', handleMouseUp);
88+
};
89+
}
90+
}, [isDragging, handleMouseMove, handleMouseUp]);
91+
1392
// Determine which terminals to show
1493
const hasFrontendOutput = frontendTerminalOutput.length > 0;
1594
const hasBackendOutput = backendTerminalOutput.length > 0;
@@ -24,7 +103,7 @@ export const Console = () => {
24103
const hasBackend = hasBackendOutput || hasBackendFolder;
25104

26105
// Show all terminals if any terminal has content (to ensure Frontend is visible when Backend/System have content)
27-
const totalTerminals = hasFrontend + hasBackend + hasMain;
106+
const totalTerminals = (hasFrontend ? 1 : 0) + (hasBackend ? 1 : 0) + (hasMain ? 1 : 0);
28107
const showAllTerminals = totalTerminals > 0;
29108

30109
// Count active terminals
@@ -57,6 +136,22 @@ export const Console = () => {
57136
);
58137
};
59138

139+
// Drag handle component
140+
const DragHandle = ({ onMouseDown, className = "" }: { onMouseDown: (e: React.MouseEvent) => void; className?: string }) => (
141+
<div
142+
className={`w-1 bg-border hover:bg-accent cursor-col-resize active:bg-primary transition-colors ${className}`}
143+
onMouseDown={onMouseDown}
144+
style={{ userSelect: 'none' }}
145+
/>
146+
);
147+
148+
// Resizable container wrapper
149+
const ResizableContainer = ({ children, className = "" }: { children: React.ReactNode; className?: string }) => (
150+
<div data-terminal-container className={`flex h-full ${className}`}>
151+
{children}
152+
</div>
153+
);
154+
60155
// Single terminal layout
61156
if (terminalCount === 1) {
62157
if (hasFrontend) {
@@ -73,58 +168,63 @@ export const Console = () => {
73168
if (hasFrontend && hasBackend) {
74169
// Frontend and Backend side by side
75170
return (
76-
<div className="flex h-full">
77-
<div className="flex-1 border-r border-border">
171+
<ResizableContainer>
172+
<div className="h-full" style={{ width: `${panelSizes.left}%` }}>
78173
<TerminalPanel title="Frontend" outputs={frontendTerminalOutput} color="green" />
79174
</div>
80-
<div className="flex-1">
175+
<DragHandle onMouseDown={(e) => handleMouseDown('left-right', e)} />
176+
<div className="h-full" style={{ width: `${panelSizes.right}%` }}>
81177
<TerminalPanel title="Backend" outputs={backendTerminalOutput} color="orange" />
82178
</div>
83-
</div>
179+
</ResizableContainer>
84180
);
85181
}
86182
if (hasMain && hasFrontend) {
87183
// System and Frontend side by side
88184
return (
89-
<div className="flex h-full">
90-
<div className="flex-1 border-r border-border">
185+
<ResizableContainer>
186+
<div className="h-full" style={{ width: `${panelSizes.left}%` }}>
91187
<TerminalPanel title="System" outputs={appOutput} color="blue" />
92188
</div>
93-
<div className="flex-1">
189+
<DragHandle onMouseDown={(e) => handleMouseDown('left-right', e)} />
190+
<div className="h-full" style={{ width: `${panelSizes.right}%` }}>
94191
<TerminalPanel title="Frontend" outputs={frontendTerminalOutput} color="green" />
95192
</div>
96-
</div>
193+
</ResizableContainer>
97194
);
98195
}
99196
if (hasMain && hasBackend) {
100197
// System and Backend side by side
101198
return (
102-
<div className="flex h-full">
103-
<div className="flex-1 border-r border-border">
199+
<ResizableContainer>
200+
<div className="h-full" style={{ width: `${panelSizes.left}%` }}>
104201
<TerminalPanel title="System" outputs={appOutput} color="blue" />
105202
</div>
106-
<div className="flex-1">
203+
<DragHandle onMouseDown={(e) => handleMouseDown('left-right', e)} />
204+
<div className="h-full" style={{ width: `${panelSizes.right}%` }}>
107205
<TerminalPanel title="Backend" outputs={backendTerminalOutput} color="orange" />
108206
</div>
109-
</div>
207+
</ResizableContainer>
110208
);
111209
}
112210
}
113211

114-
// Three terminals layout - show in a 3-column layout
212+
// Three terminals layout - show in a 3-column layout with resizable panels
115213
if (terminalCount === 3) {
116214
return (
117-
<div className="flex h-full">
118-
<div className="flex-1 border-r border-border">
215+
<ResizableContainer>
216+
<div className="h-full" style={{ width: `${panelSizes.left2}%` }}>
119217
<TerminalPanel title="System" outputs={appOutput} color="blue" />
120218
</div>
121-
<div className="flex-1 border-r border-border">
219+
<DragHandle onMouseDown={(e) => handleMouseDown('left-middle', e)} />
220+
<div className="h-full" style={{ width: `${panelSizes.middle}%` }}>
122221
<TerminalPanel title="Frontend" outputs={frontendTerminalOutput} color="green" />
123222
</div>
124-
<div className="flex-1">
223+
<DragHandle onMouseDown={(e) => handleMouseDown('middle-right', e)} />
224+
<div className="h-full" style={{ width: `${panelSizes.right2}%` }}>
125225
<TerminalPanel title="Backend" outputs={backendTerminalOutput} color="orange" />
126226
</div>
127-
</div>
227+
</ResizableContainer>
128228
);
129229
}
130230

0 commit comments

Comments
 (0)