@illuma-ai/code-sandbox 1.4.0 → 1.5.0
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/LICENSE +18 -12
- package/README.md +10 -11
- package/dist/index.cjs +91 -96
- package/dist/index.js +11895 -17954
- package/dist/styles.css +1 -1
- package/package.json +9 -12
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/src/components/BootOverlay.tsx +0 -145
- package/src/components/CodeEditor.tsx +0 -298
- package/src/components/FileTree.tsx +0 -678
- package/src/components/Preview.tsx +0 -262
- package/src/components/Terminal.tsx +0 -111
- package/src/components/ViewSlider.tsx +0 -87
- package/src/components/Workbench.tsx +0 -382
- package/src/hooks/useRuntime.ts +0 -637
- package/src/index.ts +0 -51
- package/src/services/runtime.ts +0 -775
- package/src/styles.css +0 -178
- package/src/templates/fullstack-starter.ts +0 -3507
- package/src/templates/index.ts +0 -607
- package/src/types.ts +0 -375
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workbench — Main layout component for the code sandbox.
|
|
3
|
-
*
|
|
4
|
-
* Two full-width views toggled via a pill slider in the toolbar:
|
|
5
|
-
* - Code: FileTree (left) + Editor (top-right) + Terminal (bottom-right)
|
|
6
|
-
* - Preview: Full-width browser iframe with URL bar
|
|
7
|
-
*
|
|
8
|
-
* The toggle lives inside a compact toolbar row shared with the URL bar.
|
|
9
|
-
* Both views are full-width — Preview has no file tree, Code has no iframe.
|
|
10
|
-
*
|
|
11
|
-
* Responsive: works from ~320px up. On narrow screens the file tree
|
|
12
|
-
* collapses to a smaller width, and the allotment handles overflow.
|
|
13
|
-
*
|
|
14
|
-
* @see bolt.diy: app/components/workbench/Workbench.client.tsx (reference)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import React, {
|
|
18
|
-
useMemo,
|
|
19
|
-
useState,
|
|
20
|
-
useEffect,
|
|
21
|
-
useCallback,
|
|
22
|
-
useRef,
|
|
23
|
-
useImperativeHandle,
|
|
24
|
-
forwardRef,
|
|
25
|
-
} from "react";
|
|
26
|
-
import { Allotment } from "allotment";
|
|
27
|
-
import { motion, AnimatePresence } from "framer-motion";
|
|
28
|
-
import "allotment/dist/style.css";
|
|
29
|
-
|
|
30
|
-
import { buildFileTree, FileTree } from "./FileTree";
|
|
31
|
-
import { CodeEditor } from "./CodeEditor";
|
|
32
|
-
import { Terminal } from "./Terminal";
|
|
33
|
-
import { Preview } from "./Preview";
|
|
34
|
-
import { BootOverlay } from "./BootOverlay";
|
|
35
|
-
import { ViewSlider, type WorkbenchView } from "./ViewSlider";
|
|
36
|
-
import { useRuntime } from "../hooks/useRuntime";
|
|
37
|
-
import type {
|
|
38
|
-
CodeSandboxProps,
|
|
39
|
-
CodeSandboxHandle,
|
|
40
|
-
FileChangeStatus,
|
|
41
|
-
} from "../types";
|
|
42
|
-
|
|
43
|
-
/** Transition for view crossfade */
|
|
44
|
-
const VIEW_TRANSITION = {
|
|
45
|
-
duration: 0.2,
|
|
46
|
-
ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* CodeSandbox — The main entrypoint component.
|
|
51
|
-
*
|
|
52
|
-
* Renders a complete IDE-like workbench with two full-width views:
|
|
53
|
-
* - Code: file tree + editor + terminal (split panes)
|
|
54
|
-
* - Preview: full-width browser iframe
|
|
55
|
-
*
|
|
56
|
-
* A compact toolbar row contains the view toggle and (in preview mode)
|
|
57
|
-
* a URL bar with refresh button.
|
|
58
|
-
*/
|
|
59
|
-
export const CodeSandbox = forwardRef<CodeSandboxHandle, CodeSandboxProps>(
|
|
60
|
-
function CodeSandbox(props, ref) {
|
|
61
|
-
const { className = "", height = "100vh", port = 3000 } = props;
|
|
62
|
-
|
|
63
|
-
const runtime = useRuntime(props);
|
|
64
|
-
const {
|
|
65
|
-
state,
|
|
66
|
-
selectedFile,
|
|
67
|
-
openFiles,
|
|
68
|
-
handleFileChange,
|
|
69
|
-
handleSelectFile,
|
|
70
|
-
handleCloseFile,
|
|
71
|
-
handleBrowserError,
|
|
72
|
-
restart,
|
|
73
|
-
updateFiles,
|
|
74
|
-
updateFile,
|
|
75
|
-
getFiles,
|
|
76
|
-
getChangedFiles,
|
|
77
|
-
getFileChanges,
|
|
78
|
-
getErrors,
|
|
79
|
-
getState,
|
|
80
|
-
} = runtime;
|
|
81
|
-
|
|
82
|
-
// Expose imperative handle to parent via ref
|
|
83
|
-
useImperativeHandle(
|
|
84
|
-
ref,
|
|
85
|
-
() => ({
|
|
86
|
-
updateFiles,
|
|
87
|
-
updateFile,
|
|
88
|
-
restart,
|
|
89
|
-
getFiles,
|
|
90
|
-
getChangedFiles,
|
|
91
|
-
getFileChanges,
|
|
92
|
-
getErrors,
|
|
93
|
-
getState,
|
|
94
|
-
}),
|
|
95
|
-
[
|
|
96
|
-
updateFiles,
|
|
97
|
-
updateFile,
|
|
98
|
-
restart,
|
|
99
|
-
getFiles,
|
|
100
|
-
getChangedFiles,
|
|
101
|
-
getFileChanges,
|
|
102
|
-
getErrors,
|
|
103
|
-
getState,
|
|
104
|
-
],
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const fileTree = useMemo(() => buildFileTree(state.files), [state.files]);
|
|
108
|
-
const isBooting = state.status !== "ready" && state.status !== "error";
|
|
109
|
-
|
|
110
|
-
// Active view: code or preview
|
|
111
|
-
const [activeView, setActiveView] = useState<WorkbenchView>("code");
|
|
112
|
-
|
|
113
|
-
// Auto-switch to preview when server becomes ready
|
|
114
|
-
const hasPreview = !!state.previewUrl;
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
if (hasPreview) {
|
|
117
|
-
setActiveView("preview");
|
|
118
|
-
}
|
|
119
|
-
}, [hasPreview]);
|
|
120
|
-
|
|
121
|
-
// Switch to code view when a file is selected from the tree
|
|
122
|
-
const onFileSelect = useCallback(
|
|
123
|
-
(path: string) => {
|
|
124
|
-
handleSelectFile(path);
|
|
125
|
-
setActiveView("code");
|
|
126
|
-
},
|
|
127
|
-
[handleSelectFile],
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<div
|
|
132
|
-
className={`sb-root relative w-full bg-sb-bg text-sb-text overflow-hidden flex flex-col ${className}`}
|
|
133
|
-
style={{ height }}
|
|
134
|
-
>
|
|
135
|
-
{/* Boot overlay */}
|
|
136
|
-
{isBooting && <BootOverlay progress={state.progress} />}
|
|
137
|
-
|
|
138
|
-
{/* Toolbar — compact row with view toggle + URL bar */}
|
|
139
|
-
<Toolbar
|
|
140
|
-
activeView={activeView}
|
|
141
|
-
onViewChange={setActiveView}
|
|
142
|
-
previewUrl={state.previewUrl}
|
|
143
|
-
port={port}
|
|
144
|
-
onRefresh={restart}
|
|
145
|
-
/>
|
|
146
|
-
|
|
147
|
-
{/* Views — full width, toggled */}
|
|
148
|
-
<div className="flex-1 relative overflow-hidden min-h-0">
|
|
149
|
-
{/* Code view */}
|
|
150
|
-
<motion.div
|
|
151
|
-
className="absolute inset-0"
|
|
152
|
-
initial={false}
|
|
153
|
-
animate={{
|
|
154
|
-
opacity: activeView === "code" ? 1 : 0,
|
|
155
|
-
pointerEvents: activeView === "code" ? "auto" : "none",
|
|
156
|
-
}}
|
|
157
|
-
transition={{ duration: 0.15 }}
|
|
158
|
-
>
|
|
159
|
-
<CodeView
|
|
160
|
-
fileTree={fileTree}
|
|
161
|
-
files={state.files}
|
|
162
|
-
originalFiles={state.originalFiles}
|
|
163
|
-
fileChanges={state.fileChanges}
|
|
164
|
-
selectedFile={selectedFile}
|
|
165
|
-
openFiles={openFiles}
|
|
166
|
-
terminalOutput={state.terminalOutput}
|
|
167
|
-
onSelectFile={onFileSelect}
|
|
168
|
-
onCloseFile={handleCloseFile}
|
|
169
|
-
onFileChange={handleFileChange}
|
|
170
|
-
/>
|
|
171
|
-
</motion.div>
|
|
172
|
-
|
|
173
|
-
{/* Preview view */}
|
|
174
|
-
<motion.div
|
|
175
|
-
className="absolute inset-0"
|
|
176
|
-
initial={false}
|
|
177
|
-
animate={{
|
|
178
|
-
opacity: activeView === "preview" ? 1 : 0,
|
|
179
|
-
pointerEvents: activeView === "preview" ? "auto" : "none",
|
|
180
|
-
}}
|
|
181
|
-
transition={{ duration: 0.15 }}
|
|
182
|
-
>
|
|
183
|
-
<Preview
|
|
184
|
-
url={state.previewUrl}
|
|
185
|
-
onBrowserError={handleBrowserError}
|
|
186
|
-
reloadKey={state.previewReloadKey}
|
|
187
|
-
/>
|
|
188
|
-
</motion.div>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
);
|
|
192
|
-
},
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
CodeSandbox.displayName = "CodeSandbox";
|
|
196
|
-
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
|
-
// Toolbar
|
|
199
|
-
// ---------------------------------------------------------------------------
|
|
200
|
-
|
|
201
|
-
interface ToolbarProps {
|
|
202
|
-
activeView: WorkbenchView;
|
|
203
|
-
onViewChange: (view: WorkbenchView) => void;
|
|
204
|
-
previewUrl: string | null;
|
|
205
|
-
port: number;
|
|
206
|
-
onRefresh?: () => void;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Formats the internal preview URL into a friendly display URL.
|
|
211
|
-
*
|
|
212
|
-
* The actual preview URL is something like `https://host/__preview__/3000/path`
|
|
213
|
-
* (a Service Worker proxy URL). This is meaningless to the user — the app runs
|
|
214
|
-
* entirely in their browser. We show `localhost:{port}/path` instead, which
|
|
215
|
-
* matches what they'd see from a real local dev server.
|
|
216
|
-
*/
|
|
217
|
-
function formatDisplayUrl(previewUrl: string | null, port: number): string {
|
|
218
|
-
if (!previewUrl) return "";
|
|
219
|
-
|
|
220
|
-
// Extract the path portion after /__preview__/PORT or /__virtual__/PORT
|
|
221
|
-
const match = previewUrl.match(/\/__(?:preview|virtual)__\/\d+(\/.*)?$/);
|
|
222
|
-
const path = match?.[1] ?? "/";
|
|
223
|
-
return `localhost:${port}${path}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Toolbar — single compact row with view toggle + URL bar.
|
|
228
|
-
* URL bar only shows in preview mode. In code mode the space is
|
|
229
|
-
* used for a subtle status label.
|
|
230
|
-
*/
|
|
231
|
-
function Toolbar({
|
|
232
|
-
activeView,
|
|
233
|
-
onViewChange,
|
|
234
|
-
previewUrl,
|
|
235
|
-
port,
|
|
236
|
-
onRefresh,
|
|
237
|
-
}: ToolbarProps) {
|
|
238
|
-
const displayUrl = formatDisplayUrl(previewUrl, port);
|
|
239
|
-
|
|
240
|
-
return (
|
|
241
|
-
<div className="flex items-center gap-2 px-2 py-1 bg-sb-bg-alt border-b border-sb-border min-h-[36px] shrink-0">
|
|
242
|
-
{/* View toggle — always visible */}
|
|
243
|
-
<ViewSlider selected={activeView} onSelect={onViewChange} />
|
|
244
|
-
|
|
245
|
-
{/* Right side: URL bar in preview mode, status in code mode */}
|
|
246
|
-
<div className="flex-1 min-w-0">
|
|
247
|
-
{activeView === "preview" ? (
|
|
248
|
-
<div className="flex items-center gap-1.5">
|
|
249
|
-
<button
|
|
250
|
-
className="shrink-0 p-1 rounded text-sb-text-muted hover:text-sb-text-active hover:bg-sb-bg-hover transition-colors text-xs"
|
|
251
|
-
onClick={onRefresh}
|
|
252
|
-
title="Refresh preview"
|
|
253
|
-
>
|
|
254
|
-
<RefreshIcon />
|
|
255
|
-
</button>
|
|
256
|
-
<div className="flex-1 min-w-0 bg-sb-bg rounded px-2 py-0.5 text-xs text-sb-text-muted font-mono truncate">
|
|
257
|
-
{previewUrl ? displayUrl : "Waiting for server..."}
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
) : (
|
|
261
|
-
<div className="text-xs text-sb-text-muted px-1 truncate">Editor</div>
|
|
262
|
-
)}
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** Small inline refresh icon (SVG) */
|
|
269
|
-
function RefreshIcon() {
|
|
270
|
-
return (
|
|
271
|
-
<svg
|
|
272
|
-
width="14"
|
|
273
|
-
height="14"
|
|
274
|
-
viewBox="0 0 24 24"
|
|
275
|
-
fill="none"
|
|
276
|
-
stroke="currentColor"
|
|
277
|
-
strokeWidth="2"
|
|
278
|
-
strokeLinecap="round"
|
|
279
|
-
strokeLinejoin="round"
|
|
280
|
-
>
|
|
281
|
-
<polyline points="23 4 23 10 17 10" />
|
|
282
|
-
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
283
|
-
</svg>
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
// CodeView — full-width code editing layout
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
|
|
291
|
-
interface CodeViewProps {
|
|
292
|
-
fileTree: ReturnType<typeof buildFileTree>;
|
|
293
|
-
files: Record<string, string>;
|
|
294
|
-
originalFiles: Record<string, string>;
|
|
295
|
-
fileChanges: Record<string, FileChangeStatus>;
|
|
296
|
-
selectedFile: string | null;
|
|
297
|
-
openFiles: string[];
|
|
298
|
-
terminalOutput: string[];
|
|
299
|
-
onSelectFile: (path: string) => void;
|
|
300
|
-
onCloseFile: (path: string) => void;
|
|
301
|
-
onFileChange: (path: string, content: string) => void;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* CodeView — FileTree (left) + Editor (top-right) + Terminal (bottom-right).
|
|
306
|
-
* Uses Allotment for resizable split panes.
|
|
307
|
-
* Terminal can be minimized to just a header bar.
|
|
308
|
-
* Responsive: file tree shrinks on narrow containers.
|
|
309
|
-
*/
|
|
310
|
-
function CodeView({
|
|
311
|
-
fileTree,
|
|
312
|
-
files,
|
|
313
|
-
originalFiles,
|
|
314
|
-
fileChanges,
|
|
315
|
-
selectedFile,
|
|
316
|
-
openFiles,
|
|
317
|
-
terminalOutput,
|
|
318
|
-
onSelectFile,
|
|
319
|
-
onCloseFile,
|
|
320
|
-
onFileChange,
|
|
321
|
-
}: CodeViewProps) {
|
|
322
|
-
const [terminalMinimized, setTerminalMinimized] = useState(false);
|
|
323
|
-
|
|
324
|
-
return (
|
|
325
|
-
<div className="h-full w-full">
|
|
326
|
-
<Allotment>
|
|
327
|
-
{/* File tree — left */}
|
|
328
|
-
<Allotment.Pane preferredSize={200} minSize={120} maxSize={350}>
|
|
329
|
-
<FileTree
|
|
330
|
-
files={fileTree}
|
|
331
|
-
selectedFile={selectedFile}
|
|
332
|
-
onSelectFile={onSelectFile}
|
|
333
|
-
fileChanges={fileChanges}
|
|
334
|
-
/>
|
|
335
|
-
</Allotment.Pane>
|
|
336
|
-
|
|
337
|
-
{/* Editor + Terminal — right */}
|
|
338
|
-
<Allotment.Pane>
|
|
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>
|
|
360
|
-
|
|
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>
|
|
378
|
-
</Allotment.Pane>
|
|
379
|
-
</Allotment>
|
|
380
|
-
</div>
|
|
381
|
-
);
|
|
382
|
-
}
|