@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.
@@ -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
- }