Skip to content

Commit ecc69b0

Browse files
committed
Resizable Console Update
1 parent 6db3899 commit ecc69b0

File tree

2 files changed

+153
-72
lines changed

2 files changed

+153
-72
lines changed

src/components/ConsoleDrawer/ConsoleDrawer.tsx

Lines changed: 94 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// ConsoleDrawer component - displays captured console logs from preview iframe with REPL
22

33
import { useState, useRef, useEffect } from "react";
4+
import { useVerticalResizable } from "../../hooks/useVerticalResizable";
45

56
export type LogLevel = "log" | "warn" | "error" | "info" | "result" | "command";
67

@@ -26,6 +27,9 @@ function ConsoleDrawer({ logs, onClear, isOpen, onToggle, onExecute }: ConsoleDr
2627
const inputRef = useRef<HTMLInputElement>(null);
2728
const outputRef = useRef<HTMLDivElement>(null);
2829

30+
// Vertical resizing
31+
const { height, startResizing, isResizing } = useVerticalResizable(192, 48, 600);
32+
2933
// Auto-scroll to bottom when new logs appear
3034
useEffect(() => {
3135
if (outputRef.current) {
@@ -129,87 +133,105 @@ function ConsoleDrawer({ logs, onClear, isOpen, onToggle, onExecute }: ConsoleDr
129133
}
130134

131135
return (
132-
<div className="flex flex-col h-48 bg-slate-900 border-t border-slate-700 transition-all duration-300">
133-
{/* Console Header */}
134-
<div className="flex items-center justify-between px-4 py-1 bg-slate-800 border-b border-slate-700">
135-
<button
136-
onClick={onToggle}
137-
className="text-xs font-bold text-slate-300 hover:text-white"
138-
>
139-
⌄ Console
140-
</button>
141-
<div className="flex items-center gap-2">
136+
<>
137+
{/* Global overlay during resize */}
138+
{isResizing && (
139+
<div
140+
className="fixed inset-0 z-[9999] cursor-row-resize"
141+
style={{ background: 'transparent' }}
142+
/>
143+
)}
144+
<div
145+
className="flex flex-col bg-slate-900 border-t border-slate-700 transition-all duration-75 relative"
146+
style={{ height: height }}
147+
>
148+
{/* Resize Handle */}
149+
<div
150+
className="absolute top-0 left-0 right-0 h-1 cursor-row-resize z-10 hover:bg-blue-500/50 transition-colors"
151+
onMouseDown={startResizing}
152+
/>
153+
154+
{/* Console Header */}
155+
<div className="flex items-center justify-between px-4 py-1 bg-slate-800 border-b border-slate-700 select-none">
142156
<button
143-
onClick={onClear}
144-
className="text-[10px] uppercase font-bold text-slate-500 hover:text-slate-300 px-2 py-1 rounded hover:bg-slate-700 transition-colors"
157+
onClick={onToggle}
158+
className="text-xs font-bold text-slate-300 hover:text-white"
145159
>
146-
Clear
160+
⌄ Console
147161
</button>
162+
<div className="flex items-center gap-2">
163+
<button
164+
onClick={onClear}
165+
className="text-[10px] uppercase font-bold text-slate-500 hover:text-slate-300 px-2 py-1 rounded hover:bg-slate-700 transition-colors"
166+
>
167+
Clear
168+
</button>
169+
</div>
148170
</div>
149-
</div>
150171

151-
{/* Console Output */}
152-
<div ref={outputRef} className="flex-1 overflow-auto p-2 font-mono text-xs">
153-
{logs.length === 0 ? (
154-
<div className="text-slate-600 italic px-2">No logs yet. Type JavaScript below to execute in the preview context...</div>
155-
) : (
156-
logs.map((log) => (
157-
<div
158-
key={log.id}
159-
className={`flex items-start gap-2 border-b border-slate-800/50 py-1 px-2 ${getLevelStyles(log.level)}`}
160-
>
161-
{log.level !== "command" && log.level !== "result" && (
162-
<span className="opacity-50 min-w-[50px]">
163-
{new Date(log.timestamp).toLocaleTimeString([], {
164-
hour12: false,
165-
hour: "2-digit",
166-
minute: "2-digit",
167-
second: "2-digit",
168-
})}
169-
</span>
170-
)}
171-
{(log.level === "command" || log.level === "result" || log.level === "error" || log.level === "warn") && (
172-
<span className={`font-bold ${log.level === "result" ? "text-blue-400" : log.level === "command" ? "text-slate-500" : ""}`}>
173-
{getLevelPrefix(log.level)}
174-
</span>
175-
)}
176-
<div className="flex-1 whitespace-pre-wrap break-words">
177-
{log.messages.map((msg, i) => (
178-
<span key={i} className="mr-2">
179-
{typeof msg === "object"
180-
? JSON.stringify(msg, null, 2)
181-
: String(msg)}
172+
{/* Console Output */}
173+
<div ref={outputRef} className="flex-1 overflow-auto p-2 font-mono text-xs">
174+
{logs.length === 0 ? (
175+
<div className="text-slate-600 italic px-2">No logs yet. Type JavaScript below to execute in the preview context...</div>
176+
) : (
177+
logs.map((log) => (
178+
<div
179+
key={log.id}
180+
className={`flex items-start gap-2 border-b border-slate-800/50 py-1 px-2 ${getLevelStyles(log.level)}`}
181+
>
182+
{log.level !== "command" && log.level !== "result" && (
183+
<span className="opacity-50 min-w-[50px]">
184+
{new Date(log.timestamp).toLocaleTimeString([], {
185+
hour12: false,
186+
hour: "2-digit",
187+
minute: "2-digit",
188+
second: "2-digit",
189+
})}
182190
</span>
183-
))}
191+
)}
192+
{(log.level === "command" || log.level === "result" || log.level === "error" || log.level === "warn") && (
193+
<span className={`font-bold ${log.level === "result" ? "text-blue-400" : log.level === "command" ? "text-slate-500" : ""}`}>
194+
{getLevelPrefix(log.level)}
195+
</span>
196+
)}
197+
<div className="flex-1 whitespace-pre-wrap break-words">
198+
{log.messages.map((msg, i) => (
199+
<span key={i} className="mr-2">
200+
{typeof msg === "object"
201+
? JSON.stringify(msg, null, 2)
202+
: String(msg)}
203+
</span>
204+
))}
205+
</div>
184206
</div>
185-
</div>
186-
))
187-
)}
188-
</div>
207+
))
208+
)}
209+
</div>
189210

190-
{/* REPL Input */}
191-
<div className="flex items-center gap-2 px-2 py-1.5 bg-slate-800/50 border-t border-slate-700">
192-
<span className="text-blue-400 font-mono text-xs font-bold"></span>
193-
<input
194-
ref={inputRef}
195-
type="text"
196-
value={inputValue}
197-
onChange={(e) => setInputValue(e.target.value)}
198-
onKeyDown={handleKeyDown}
199-
placeholder="Type JavaScript and press Enter..."
200-
className="flex-1 bg-transparent text-slate-200 font-mono text-xs outline-none placeholder:text-slate-600"
201-
spellCheck={false}
202-
autoComplete="off"
203-
/>
204-
<button
205-
onClick={handleExecute}
206-
disabled={!inputValue.trim()}
207-
className="text-[10px] uppercase font-bold text-slate-500 hover:text-slate-300 px-2 py-0.5 rounded hover:bg-slate-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
208-
>
209-
Run
210-
</button>
211+
{/* REPL Input */}
212+
<div className="flex items-center gap-2 px-2 py-1.5 bg-slate-800/50 border-t border-slate-700">
213+
<span className="text-blue-400 font-mono text-xs font-bold"></span>
214+
<input
215+
ref={inputRef}
216+
type="text"
217+
value={inputValue}
218+
onChange={(e) => setInputValue(e.target.value)}
219+
onKeyDown={handleKeyDown}
220+
placeholder="Type JavaScript and press Enter..."
221+
className="flex-1 bg-transparent text-slate-200 font-mono text-xs outline-none placeholder:text-slate-600"
222+
spellCheck={false}
223+
autoComplete="off"
224+
/>
225+
<button
226+
onClick={handleExecute}
227+
disabled={!inputValue.trim()}
228+
className="text-[10px] uppercase font-bold text-slate-500 hover:text-slate-300 px-2 py-0.5 rounded hover:bg-slate-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
229+
>
230+
Run
231+
</button>
232+
</div>
211233
</div>
212-
</div>
234+
</>
213235
);
214236
}
215237

src/hooks/useVerticalResizable.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useState, useCallback, useEffect } from "react";
2+
3+
/**
4+
* Hook for managing vertically resizable drawers rooted at the bottom of the screen.
5+
* @param initialHeight - Initial height in pixels (default: 192 - equivalent to h-48)
6+
* @param minHeight - Minimum height in pixels (default: 48)
7+
* @param maxHeight - Maximum height in pixels (default: 800)
8+
*/
9+
export function useVerticalResizable(
10+
initialHeight: number = 192,
11+
minHeight: number = 48,
12+
maxHeight: number = 800
13+
) {
14+
const [height, setHeight] = useState(initialHeight);
15+
const [isResizing, setIsResizing] = useState(false);
16+
17+
const startResizing = useCallback(() => {
18+
setIsResizing(true);
19+
}, []);
20+
21+
const stopResizing = useCallback(() => {
22+
setIsResizing(false);
23+
}, []);
24+
25+
const resize = useCallback(
26+
(event: MouseEvent) => {
27+
if (isResizing) {
28+
// Since it's a bottom drawer, height is distance from bottom
29+
const newHeight = window.innerHeight - event.clientY;
30+
if (newHeight >= minHeight && newHeight <= maxHeight) {
31+
setHeight(newHeight);
32+
}
33+
}
34+
},
35+
[isResizing, minHeight, maxHeight]
36+
);
37+
38+
useEffect(() => {
39+
if (isResizing) {
40+
window.addEventListener("mousemove", resize);
41+
window.addEventListener("mouseup", stopResizing);
42+
// Prevent text selection while resizing
43+
document.body.style.cursor = "row-resize";
44+
document.body.style.userSelect = "none";
45+
} else {
46+
document.body.style.cursor = "";
47+
document.body.style.userSelect = "";
48+
}
49+
50+
return () => {
51+
window.removeEventListener("mousemove", resize);
52+
window.removeEventListener("mouseup", stopResizing);
53+
document.body.style.cursor = "";
54+
document.body.style.userSelect = "";
55+
};
56+
}, [isResizing, resize, stopResizing]);
57+
58+
return { height, startResizing, isResizing };
59+
}

0 commit comments

Comments
 (0)