@illuma-ai/code-sandbox 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Preview.d.ts +1 -1
- package/dist/components/Terminal.d.ts +6 -15
- package/dist/index.cjs +80 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8616 -8342
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types.d.ts +16 -0
- package/package.json +1 -1
- package/src/components/FileTree.tsx +364 -34
- package/src/components/Preview.tsx +30 -1
- package/src/components/Terminal.tsx +69 -210
- package/src/components/Workbench.tsx +42 -17
- package/src/hooks/useRuntime.ts +80 -16
- package/src/styles.css +1 -42
- package/src/types.ts +16 -0
|
@@ -1,252 +1,111 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Terminal —
|
|
3
|
-
*
|
|
4
|
-
* Renders process stdout/stderr with color-coded output:
|
|
5
|
-
* - Commands ($ ...) get a green prompt + white command
|
|
6
|
-
* - stderr lines get red text
|
|
7
|
-
* - Success markers (checkmarks) get green
|
|
8
|
-
* - Warnings get yellow
|
|
9
|
-
* - Package names get purple
|
|
10
|
-
* - URLs and paths get cyan
|
|
11
|
-
* - Exit codes get colored by status
|
|
2
|
+
* Terminal — Clean monochrome terminal output panel with minimize toggle.
|
|
12
3
|
*
|
|
13
4
|
* Uses a simple div-based approach (not xterm.js) to keep the bundle lean.
|
|
14
5
|
* Auto-scrolls to bottom as new output arrives.
|
|
6
|
+
* Minimize/expand state is controlled by the parent via props.
|
|
15
7
|
*/
|
|
16
8
|
|
|
17
9
|
import React, { useEffect, useRef } from "react";
|
|
18
10
|
import type { TerminalProps } from "../types";
|
|
19
11
|
|
|
20
|
-
/** Classify a terminal output line for color coding */
|
|
21
|
-
type LineType =
|
|
22
|
-
| "prompt" // $ command
|
|
23
|
-
| "stderr" // [stderr] ...
|
|
24
|
-
| "success" // ✓ ...
|
|
25
|
-
| "error" // ✗ ... or error messages
|
|
26
|
-
| "warning" // warn / Warning
|
|
27
|
-
| "info" // Installing, Dependencies, info lines
|
|
28
|
-
| "exit-ok" // Process exited with code 0
|
|
29
|
-
| "exit-fail" // Process exited with non-zero code
|
|
30
|
-
| "dim" // Empty or separator lines
|
|
31
|
-
| "normal"; // Default output
|
|
32
|
-
|
|
33
12
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Pattern matching order matters — more specific patterns first.
|
|
37
|
-
*/
|
|
38
|
-
function classifyLine(line: string): LineType {
|
|
39
|
-
const trimmed = line.trim();
|
|
40
|
-
|
|
41
|
-
// Empty or whitespace-only
|
|
42
|
-
if (!trimmed) return "dim";
|
|
43
|
-
|
|
44
|
-
// Explicit stderr prefix from runtime
|
|
45
|
-
if (trimmed.startsWith("[stderr]")) return "stderr";
|
|
46
|
-
|
|
47
|
-
// Command prompt ($ node server.js)
|
|
48
|
-
if (/^\$\s/.test(trimmed)) return "prompt";
|
|
49
|
-
|
|
50
|
-
// Success indicators
|
|
51
|
-
if (/^\s*✓/.test(line) || /^\s*✔/.test(line) || /^done$/i.test(trimmed))
|
|
52
|
-
return "success";
|
|
53
|
-
|
|
54
|
-
// Error indicators
|
|
55
|
-
if (
|
|
56
|
-
/^\s*✗/.test(line) ||
|
|
57
|
-
/^\s*✘/.test(line) ||
|
|
58
|
-
/^error\b/i.test(trimmed) ||
|
|
59
|
-
/^Error:/i.test(trimmed) ||
|
|
60
|
-
/^TypeError:/i.test(trimmed) ||
|
|
61
|
-
/^ReferenceError:/i.test(trimmed) ||
|
|
62
|
-
/^SyntaxError:/i.test(trimmed) ||
|
|
63
|
-
/ENOENT|EACCES|ECONNREFUSED/i.test(trimmed)
|
|
64
|
-
)
|
|
65
|
-
return "error";
|
|
66
|
-
|
|
67
|
-
// Process exit
|
|
68
|
-
if (/^Process exited with code\s+(\d+)/.test(trimmed)) {
|
|
69
|
-
const code = trimmed.match(/code\s+(\d+)/)?.[1];
|
|
70
|
-
return code === "0" ? "exit-ok" : "exit-fail";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Warning
|
|
74
|
-
if (
|
|
75
|
-
/^warn\b/i.test(trimmed) ||
|
|
76
|
-
/^warning\b/i.test(trimmed) ||
|
|
77
|
-
/^Warning:/i.test(trimmed) ||
|
|
78
|
-
/deprecated/i.test(trimmed)
|
|
79
|
-
)
|
|
80
|
-
return "warning";
|
|
81
|
-
|
|
82
|
-
// Info / install progress
|
|
83
|
-
if (
|
|
84
|
-
/^Installing\b/i.test(trimmed) ||
|
|
85
|
-
/^Dependencies\s+installed/i.test(trimmed) ||
|
|
86
|
-
/^Listening\b/i.test(trimmed) ||
|
|
87
|
-
/^Server\s+(running|started|listening)/i.test(trimmed) ||
|
|
88
|
-
/^Starting\b/i.test(trimmed)
|
|
89
|
-
)
|
|
90
|
-
return "info";
|
|
91
|
-
|
|
92
|
-
return "normal";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** CSS class name for each line type */
|
|
96
|
-
const LINE_CLASS: Record<LineType, string> = {
|
|
97
|
-
prompt: "", // handled specially (split into prompt + command)
|
|
98
|
-
stderr: "sb-term-stderr",
|
|
99
|
-
success: "sb-term-success",
|
|
100
|
-
error: "sb-term-stderr",
|
|
101
|
-
warning: "sb-term-warning",
|
|
102
|
-
info: "sb-term-info",
|
|
103
|
-
"exit-ok": "sb-term-exit-ok",
|
|
104
|
-
"exit-fail": "sb-term-exit-fail",
|
|
105
|
-
dim: "sb-term-dim",
|
|
106
|
-
normal: "sb-term-line",
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Render a single terminal line with appropriate styling.
|
|
111
|
-
*
|
|
112
|
-
* Prompt lines are split into a green "$ " prefix and white command text.
|
|
113
|
-
* Stderr lines strip the "[stderr]" prefix and show in red.
|
|
114
|
-
* Package install lines highlight the package name in purple.
|
|
115
|
-
*/
|
|
116
|
-
function TerminalLine({ line }: { line: string }) {
|
|
117
|
-
const type = classifyLine(line);
|
|
118
|
-
|
|
119
|
-
// Prompt line: split into green prompt + white command
|
|
120
|
-
if (type === "prompt") {
|
|
121
|
-
const commandText = line.replace(/^\$\s*/, "");
|
|
122
|
-
return (
|
|
123
|
-
<div className="whitespace-pre-wrap">
|
|
124
|
-
<span className="sb-term-prompt">{"$ "}</span>
|
|
125
|
-
<span className="sb-term-cmd">{commandText}</span>
|
|
126
|
-
</div>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Stderr: strip prefix, show in red
|
|
131
|
-
if (type === "stderr") {
|
|
132
|
-
const message = line.replace(/^\[stderr\]\s*/, "");
|
|
133
|
-
return (
|
|
134
|
-
<div className="whitespace-pre-wrap sb-term-stderr">
|
|
135
|
-
<span className="sb-term-dim select-none">{"stderr "}</span>
|
|
136
|
-
{message}
|
|
137
|
-
</div>
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Package install success: " ✓ express@4.18.0" → green check + purple pkg
|
|
142
|
-
if (type === "success" && /✓\s+\S+@/.test(line)) {
|
|
143
|
-
const match = line.match(/(.*✓\s*)(\S+@\S+)(.*)/);
|
|
144
|
-
if (match) {
|
|
145
|
-
return (
|
|
146
|
-
<div className="whitespace-pre-wrap sb-term-success">
|
|
147
|
-
{match[1]}
|
|
148
|
-
<span className="sb-term-pkg">{match[2]}</span>
|
|
149
|
-
{match[3]}
|
|
150
|
-
</div>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Info lines with port numbers: highlight the port
|
|
156
|
-
if (type === "info" && /:\d{4}/.test(line)) {
|
|
157
|
-
const parts = line.split(/(:\d{4,5})/);
|
|
158
|
-
return (
|
|
159
|
-
<div className="whitespace-pre-wrap sb-term-info">
|
|
160
|
-
{parts.map((part, i) =>
|
|
161
|
-
/^:\d{4,5}$/.test(part) ? (
|
|
162
|
-
<span key={i} className="sb-term-path">
|
|
163
|
-
{part}
|
|
164
|
-
</span>
|
|
165
|
-
) : (
|
|
166
|
-
<span key={i}>{part}</span>
|
|
167
|
-
),
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div className={`whitespace-pre-wrap ${LINE_CLASS[type]}`}>{line}</div>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Terminal component — renders process output in a git-bash inspired panel.
|
|
13
|
+
* Terminal component — renders process output in a clean monochrome panel.
|
|
180
14
|
*
|
|
181
15
|
* Features:
|
|
182
|
-
* -
|
|
183
|
-
* - Monospace font stack (Cascadia Code → JetBrains Mono → Fira Code → fallback)
|
|
16
|
+
* - Monospace font with proper line spacing
|
|
184
17
|
* - Auto-scroll to bottom on new output
|
|
18
|
+
* - Minimize/expand toggle via chevron in header (parent-controlled)
|
|
185
19
|
* - Modern thin scrollbar matching the sandbox theme
|
|
186
|
-
* - Dark background separate from editor for visual distinction
|
|
187
20
|
*/
|
|
188
|
-
export function Terminal({
|
|
21
|
+
export function Terminal({
|
|
22
|
+
output,
|
|
23
|
+
className = "",
|
|
24
|
+
minimized = false,
|
|
25
|
+
onToggleMinimize,
|
|
26
|
+
}: TerminalProps) {
|
|
189
27
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
190
28
|
|
|
191
29
|
// Auto-scroll to bottom when new output arrives
|
|
192
30
|
useEffect(() => {
|
|
193
31
|
const el = containerRef.current;
|
|
194
|
-
if (el) {
|
|
32
|
+
if (el && !minimized) {
|
|
195
33
|
el.scrollTop = el.scrollHeight;
|
|
196
34
|
}
|
|
197
|
-
}, [output.length]);
|
|
35
|
+
}, [output.length, minimized]);
|
|
198
36
|
|
|
199
37
|
return (
|
|
200
38
|
<div
|
|
201
|
-
className={`flex flex-col h-full ${className}`}
|
|
39
|
+
className={`flex flex-col ${minimized ? "" : "h-full"} ${className}`}
|
|
202
40
|
style={{ background: "var(--sb-terminal)" }}
|
|
203
41
|
>
|
|
204
|
-
{/* Header
|
|
42
|
+
{/* Header */}
|
|
205
43
|
<div
|
|
206
|
-
className="flex items-center gap-2 px-3 py-1
|
|
44
|
+
className="flex items-center gap-2 px-3 py-1 border-t border-sb-border shrink-0 select-none cursor-pointer"
|
|
207
45
|
style={{ background: "var(--sb-terminal-header)" }}
|
|
46
|
+
onClick={onToggleMinimize}
|
|
208
47
|
>
|
|
209
|
-
{/*
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
48
|
+
{/* Chevron toggle */}
|
|
49
|
+
<svg
|
|
50
|
+
width="14"
|
|
51
|
+
height="14"
|
|
52
|
+
viewBox="0 0 16 16"
|
|
53
|
+
fill="none"
|
|
54
|
+
className="shrink-0 transition-transform duration-150"
|
|
55
|
+
style={{
|
|
56
|
+
transform: minimized ? "rotate(-90deg)" : "rotate(0deg)",
|
|
57
|
+
color: "var(--sb-text-muted)",
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<path
|
|
61
|
+
d="M4 6l4 4 4-4"
|
|
62
|
+
stroke="currentColor"
|
|
63
|
+
strokeWidth="1.5"
|
|
64
|
+
strokeLinecap="round"
|
|
65
|
+
strokeLinejoin="round"
|
|
222
66
|
/>
|
|
223
|
-
</
|
|
67
|
+
</svg>
|
|
224
68
|
<span
|
|
225
69
|
className="text-[11px] font-medium tracking-wider"
|
|
226
70
|
style={{ color: "var(--sb-text-muted)" }}
|
|
227
71
|
>
|
|
228
72
|
TERMINAL
|
|
229
73
|
</span>
|
|
230
|
-
{/*
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
ref={containerRef}
|
|
239
|
-
className="flex-1 overflow-auto px-3.5 py-2.5 sb-terminal-output overscroll-contain"
|
|
240
|
-
>
|
|
241
|
-
{output.length === 0 ? (
|
|
242
|
-
<div className="flex items-center gap-2 sb-term-dim">
|
|
243
|
-
<span className="sb-term-prompt">$</span>
|
|
244
|
-
<span className="animate-pulse">_</span>
|
|
245
|
-
</div>
|
|
246
|
-
) : (
|
|
247
|
-
output.map((line, i) => <TerminalLine key={i} line={line} />)
|
|
74
|
+
{/* Line count badge */}
|
|
75
|
+
{output.length > 0 && (
|
|
76
|
+
<span
|
|
77
|
+
className="text-[10px] ml-auto tabular-nums"
|
|
78
|
+
style={{ color: "var(--sb-text-muted)", opacity: 0.6 }}
|
|
79
|
+
>
|
|
80
|
+
{output.length} lines
|
|
81
|
+
</span>
|
|
248
82
|
)}
|
|
249
83
|
</div>
|
|
84
|
+
|
|
85
|
+
{/* Output area — hidden when minimized */}
|
|
86
|
+
{!minimized && (
|
|
87
|
+
<div
|
|
88
|
+
ref={containerRef}
|
|
89
|
+
className="flex-1 overflow-auto px-3.5 py-2 sb-terminal-output overscroll-contain min-h-0"
|
|
90
|
+
>
|
|
91
|
+
{output.length === 0 ? (
|
|
92
|
+
<div style={{ color: "var(--sb-text-muted)" }}>
|
|
93
|
+
<span>$ </span>
|
|
94
|
+
<span className="animate-pulse">_</span>
|
|
95
|
+
</div>
|
|
96
|
+
) : (
|
|
97
|
+
output.map((line, i) => (
|
|
98
|
+
<div
|
|
99
|
+
key={i}
|
|
100
|
+
className="whitespace-pre-wrap"
|
|
101
|
+
style={{ color: "var(--sb-text)" }}
|
|
102
|
+
>
|
|
103
|
+
{line}
|
|
104
|
+
</div>
|
|
105
|
+
))
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
250
109
|
</div>
|
|
251
110
|
);
|
|
252
111
|
}
|
|
@@ -183,6 +183,7 @@ export const CodeSandbox = forwardRef<CodeSandboxHandle, CodeSandboxProps>(
|
|
|
183
183
|
<Preview
|
|
184
184
|
url={state.previewUrl}
|
|
185
185
|
onBrowserError={handleBrowserError}
|
|
186
|
+
reloadKey={state.previewReloadKey}
|
|
186
187
|
/>
|
|
187
188
|
</motion.div>
|
|
188
189
|
</div>
|
|
@@ -303,6 +304,7 @@ interface CodeViewProps {
|
|
|
303
304
|
/**
|
|
304
305
|
* CodeView — FileTree (left) + Editor (top-right) + Terminal (bottom-right).
|
|
305
306
|
* Uses Allotment for resizable split panes.
|
|
307
|
+
* Terminal can be minimized to just a header bar.
|
|
306
308
|
* Responsive: file tree shrinks on narrow containers.
|
|
307
309
|
*/
|
|
308
310
|
function CodeView({
|
|
@@ -317,6 +319,8 @@ function CodeView({
|
|
|
317
319
|
onCloseFile,
|
|
318
320
|
onFileChange,
|
|
319
321
|
}: CodeViewProps) {
|
|
322
|
+
const [terminalMinimized, setTerminalMinimized] = useState(false);
|
|
323
|
+
|
|
320
324
|
return (
|
|
321
325
|
<div className="h-full w-full">
|
|
322
326
|
<Allotment>
|
|
@@ -332,24 +336,45 @@ function CodeView({
|
|
|
332
336
|
|
|
333
337
|
{/* Editor + Terminal — right */}
|
|
334
338
|
<Allotment.Pane>
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
339
|
+
<div className="h-full flex flex-col">
|
|
340
|
+
{/* Editor takes remaining space */}
|
|
341
|
+
<div
|
|
342
|
+
className={
|
|
343
|
+
terminalMinimized ? "flex-1 min-h-0" : "flex-1 min-h-0"
|
|
344
|
+
}
|
|
345
|
+
style={{ height: terminalMinimized ? "100%" : "70%" }}
|
|
346
|
+
>
|
|
347
|
+
<div className="h-full">
|
|
348
|
+
<CodeEditor
|
|
349
|
+
files={files}
|
|
350
|
+
originalFiles={originalFiles}
|
|
351
|
+
fileChanges={fileChanges}
|
|
352
|
+
activeFile={selectedFile}
|
|
353
|
+
openFiles={openFiles}
|
|
354
|
+
onSelectFile={onSelectFile}
|
|
355
|
+
onCloseFile={onCloseFile}
|
|
356
|
+
onFileChange={onFileChange}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
348
360
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
361
|
+
{/* Terminal — collapsible */}
|
|
362
|
+
{terminalMinimized ? (
|
|
363
|
+
<Terminal
|
|
364
|
+
output={terminalOutput}
|
|
365
|
+
minimized={true}
|
|
366
|
+
onToggleMinimize={() => setTerminalMinimized(false)}
|
|
367
|
+
/>
|
|
368
|
+
) : (
|
|
369
|
+
<div style={{ height: "30%", minHeight: 60 }}>
|
|
370
|
+
<Terminal
|
|
371
|
+
output={terminalOutput}
|
|
372
|
+
minimized={false}
|
|
373
|
+
onToggleMinimize={() => setTerminalMinimized(true)}
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
353
378
|
</Allotment.Pane>
|
|
354
379
|
</Allotment>
|
|
355
380
|
</div>
|
package/src/hooks/useRuntime.ts
CHANGED
|
@@ -29,6 +29,38 @@ import type {
|
|
|
29
29
|
/** Debug log prefix for easy filtering in DevTools */
|
|
30
30
|
const DBG = "[CodeSandbox:useRuntime]";
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* File extensions that can be hot-reloaded (iframe refresh) without
|
|
34
|
+
* restarting the Node.js server process. These are static assets
|
|
35
|
+
* served by Express — changing them doesn't require a process restart.
|
|
36
|
+
*/
|
|
37
|
+
const HOT_RELOAD_EXTENSIONS = new Set([
|
|
38
|
+
"css",
|
|
39
|
+
"html",
|
|
40
|
+
"htm",
|
|
41
|
+
"svg",
|
|
42
|
+
"png",
|
|
43
|
+
"jpg",
|
|
44
|
+
"jpeg",
|
|
45
|
+
"gif",
|
|
46
|
+
"webp",
|
|
47
|
+
"ico",
|
|
48
|
+
"woff",
|
|
49
|
+
"woff2",
|
|
50
|
+
"ttf",
|
|
51
|
+
"eot",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check whether a file path is hot-reloadable (static asset that doesn't
|
|
56
|
+
* require a server restart). Returns false for JS/TS/JSON and any file
|
|
57
|
+
* without an extension.
|
|
58
|
+
*/
|
|
59
|
+
function isHotReloadable(filePath: string): boolean {
|
|
60
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
61
|
+
return ext ? HOT_RELOAD_EXTENSIONS.has(ext) : false;
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
/**
|
|
33
65
|
* Compute per-file change statuses between an original and current file set.
|
|
34
66
|
*
|
|
@@ -115,6 +147,7 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
115
147
|
files: {},
|
|
116
148
|
originalFiles: {},
|
|
117
149
|
fileChanges: {},
|
|
150
|
+
previewReloadKey: 0,
|
|
118
151
|
error: null,
|
|
119
152
|
errors: [],
|
|
120
153
|
});
|
|
@@ -340,12 +373,14 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
340
373
|
// Runtime exists — diff and write changed files
|
|
341
374
|
const currentFiles = runtime.getCurrentFiles();
|
|
342
375
|
let changed = false;
|
|
376
|
+
const changedPaths: string[] = [];
|
|
343
377
|
|
|
344
378
|
for (const [path, content] of Object.entries(newFiles)) {
|
|
345
379
|
if (currentFiles[path] !== content) {
|
|
346
380
|
try {
|
|
347
381
|
await runtime.writeFile(path, content);
|
|
348
382
|
changed = true;
|
|
383
|
+
changedPaths.push(path);
|
|
349
384
|
console.log(DBG, `updated file: ${path}`);
|
|
350
385
|
} catch (err) {
|
|
351
386
|
console.warn(DBG, `failed to write ${path}:`, err);
|
|
@@ -356,25 +391,54 @@ export function useRuntime(props: CodeSandboxProps) {
|
|
|
356
391
|
// Compute change status — "original" is what we had before this update
|
|
357
392
|
const fileChanges = computeFileChanges(currentFiles, newFiles);
|
|
358
393
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
394
|
+
// Decide: hot reload (iframe refresh) vs cold restart (kill + rerun server).
|
|
395
|
+
// Hot reload when ALL changed files are static assets (CSS, HTML, images).
|
|
396
|
+
// Cold restart when ANY changed file is JS/TS/JSON/etc. that affects the server.
|
|
397
|
+
const canHotReload =
|
|
398
|
+
changed &&
|
|
399
|
+
changedPaths.length > 0 &&
|
|
400
|
+
changedPaths.every(isHotReloadable);
|
|
366
401
|
|
|
367
|
-
// Restart server if any files changed
|
|
368
402
|
if (changed && shouldRestart) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
403
|
+
if (canHotReload) {
|
|
404
|
+
// Hot reload — just bump the preview reload key to refresh the iframe
|
|
405
|
+
console.log(
|
|
406
|
+
DBG,
|
|
407
|
+
`hot reload: ${changedPaths.length} static file(s) changed — refreshing preview`,
|
|
408
|
+
);
|
|
409
|
+
setState((prev) => ({
|
|
410
|
+
...prev,
|
|
411
|
+
originalFiles: currentFiles,
|
|
412
|
+
files: newFiles,
|
|
413
|
+
fileChanges,
|
|
414
|
+
previewReloadKey: prev.previewReloadKey + 1,
|
|
415
|
+
}));
|
|
416
|
+
} else {
|
|
417
|
+
// Cold restart — server code changed, need full process restart
|
|
418
|
+
console.log(
|
|
419
|
+
DBG,
|
|
420
|
+
`cold restart: ${changedPaths.length} file(s) changed — restarting server`,
|
|
421
|
+
);
|
|
422
|
+
setState((prev) => ({
|
|
423
|
+
...prev,
|
|
424
|
+
originalFiles: currentFiles,
|
|
425
|
+
files: newFiles,
|
|
426
|
+
fileChanges,
|
|
427
|
+
}));
|
|
428
|
+
try {
|
|
429
|
+
await runtime.restart();
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.error(DBG, "restart failed:", err);
|
|
432
|
+
}
|
|
377
433
|
}
|
|
434
|
+
} else {
|
|
435
|
+
// No changes or restart disabled — just update state
|
|
436
|
+
setState((prev) => ({
|
|
437
|
+
...prev,
|
|
438
|
+
originalFiles: currentFiles,
|
|
439
|
+
files: newFiles,
|
|
440
|
+
fileChanges,
|
|
441
|
+
}));
|
|
378
442
|
}
|
|
379
443
|
|
|
380
444
|
// Notify host that files have been processed
|
package/src/styles.css
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/* ---------------------------------------------------------------------------
|
|
70
|
-
* Terminal
|
|
70
|
+
* Terminal — clean monochrome styling
|
|
71
71
|
* --------------------------------------------------------------------------- */
|
|
72
72
|
|
|
73
73
|
.sb-terminal-output {
|
|
@@ -81,47 +81,6 @@
|
|
|
81
81
|
-moz-osx-font-smoothing: grayscale;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/* Terminal line colors via data attribute */
|
|
85
|
-
.sb-term-line {
|
|
86
|
-
color: #c9d1d9;
|
|
87
|
-
}
|
|
88
|
-
.sb-term-prompt {
|
|
89
|
-
color: var(--sb-success);
|
|
90
|
-
font-weight: 600;
|
|
91
|
-
}
|
|
92
|
-
.sb-term-cmd {
|
|
93
|
-
color: #f0f6fc;
|
|
94
|
-
font-weight: 500;
|
|
95
|
-
}
|
|
96
|
-
.sb-term-stderr {
|
|
97
|
-
color: var(--sb-error);
|
|
98
|
-
}
|
|
99
|
-
.sb-term-success {
|
|
100
|
-
color: var(--sb-success);
|
|
101
|
-
}
|
|
102
|
-
.sb-term-warning {
|
|
103
|
-
color: var(--sb-warning);
|
|
104
|
-
}
|
|
105
|
-
.sb-term-info {
|
|
106
|
-
color: var(--sb-info);
|
|
107
|
-
}
|
|
108
|
-
.sb-term-dim {
|
|
109
|
-
color: #484f58;
|
|
110
|
-
}
|
|
111
|
-
.sb-term-path {
|
|
112
|
-
color: var(--sb-cyan);
|
|
113
|
-
}
|
|
114
|
-
.sb-term-pkg {
|
|
115
|
-
color: var(--sb-magenta);
|
|
116
|
-
}
|
|
117
|
-
.sb-term-exit-ok {
|
|
118
|
-
color: var(--sb-success);
|
|
119
|
-
}
|
|
120
|
-
.sb-term-exit-fail {
|
|
121
|
-
color: var(--sb-error);
|
|
122
|
-
font-weight: 600;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
84
|
/*
|
|
126
85
|
* Ranger Theme Variables — bridged from ranger/client/src/style.css.
|
|
127
86
|
*
|
package/src/types.ts
CHANGED
|
@@ -150,6 +150,12 @@ export interface RuntimeState {
|
|
|
150
150
|
* Missing keys should be treated as "unchanged".
|
|
151
151
|
*/
|
|
152
152
|
fileChanges: Record<string, FileChangeStatus>;
|
|
153
|
+
/**
|
|
154
|
+
* Monotonically increasing counter that triggers an iframe reload
|
|
155
|
+
* without restarting the server process. Incremented when only
|
|
156
|
+
* hot-reloadable files change (CSS, HTML, static assets).
|
|
157
|
+
*/
|
|
158
|
+
previewReloadKey: number;
|
|
153
159
|
/** Error message if status is 'error' */
|
|
154
160
|
error: string | null;
|
|
155
161
|
/**
|
|
@@ -343,6 +349,10 @@ export interface CodeEditorProps {
|
|
|
343
349
|
export interface TerminalProps {
|
|
344
350
|
output: string[];
|
|
345
351
|
className?: string;
|
|
352
|
+
/** Whether the terminal is collapsed to just its header bar */
|
|
353
|
+
minimized?: boolean;
|
|
354
|
+
/** Called when the user clicks the minimize/expand toggle */
|
|
355
|
+
onToggleMinimize?: () => void;
|
|
346
356
|
}
|
|
347
357
|
|
|
348
358
|
export interface PreviewProps {
|
|
@@ -351,6 +361,12 @@ export interface PreviewProps {
|
|
|
351
361
|
onRefresh?: () => void;
|
|
352
362
|
/** Called when a JavaScript error occurs inside the preview iframe */
|
|
353
363
|
onBrowserError?: (error: SandboxError) => void;
|
|
364
|
+
/**
|
|
365
|
+
* Monotonically increasing counter. When this changes, the iframe
|
|
366
|
+
* is soft-reloaded (location.reload) without restarting the server.
|
|
367
|
+
* Used for hot-reloading static asset changes (CSS, HTML, images).
|
|
368
|
+
*/
|
|
369
|
+
reloadKey?: number;
|
|
354
370
|
}
|
|
355
371
|
|
|
356
372
|
export interface BootOverlayProps {
|