@illuma-ai/code-sandbox 1.3.1 → 1.4.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.
@@ -1,678 +0,0 @@
1
- /**
2
- * FileTree — Collapsible file/folder tree with icons.
3
- *
4
- * Renders a recursive tree of FileNodes. Folders expand/collapse on click.
5
- * Files are selectable and show the appropriate icon based on extension.
6
- * Uses inline SVG icons for reliable cross-platform rendering.
7
- */
8
-
9
- import React, { useState } from "react";
10
- import type {
11
- FileMap,
12
- FileNode,
13
- FileTreeProps,
14
- FileChangeStatus,
15
- } from "../types";
16
-
17
- /** Color for each file change status indicator dot */
18
- const STATUS_COLORS: Record<FileChangeStatus, string> = {
19
- new: "#4ade80", // green-400
20
- modified: "#fb923c", // orange-400
21
- deleted: "#f87171", // red-400
22
- unchanged: "transparent",
23
- };
24
-
25
- /** Short label for each file change status (one character) */
26
- const STATUS_LETTERS: Record<FileChangeStatus, string> = {
27
- new: "N",
28
- modified: "M",
29
- deleted: "D",
30
- unchanged: "",
31
- };
32
-
33
- /**
34
- * Build a tree structure from a flat FileMap.
35
- *
36
- * Converts { "routes/api.js": "...", "server.js": "..." }
37
- * into a nested FileNode[] tree.
38
- */
39
- export function buildFileTree(files: FileMap): FileNode[] {
40
- const root: FileNode[] = [];
41
- const dirs = new Map<string, FileNode>();
42
-
43
- // Sort paths so directories come before files at each level
44
- const sortedPaths = Object.keys(files).sort((a, b) => {
45
- const aParts = a.split("/");
46
- const bParts = b.split("/");
47
- for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
48
- if (aParts[i] !== bParts[i]) {
49
- const aIsDir = i < aParts.length - 1;
50
- const bIsDir = i < bParts.length - 1;
51
- if (aIsDir && !bIsDir) return -1;
52
- if (!aIsDir && bIsDir) return 1;
53
- return aParts[i].localeCompare(bParts[i]);
54
- }
55
- }
56
- return aParts.length - bParts.length;
57
- });
58
-
59
- for (const filePath of sortedPaths) {
60
- const parts = filePath.split("/");
61
-
62
- // Ensure all parent directories exist
63
- for (let i = 0; i < parts.length - 1; i++) {
64
- const dirPath = parts.slice(0, i + 1).join("/");
65
- if (!dirs.has(dirPath)) {
66
- const dirNode: FileNode = {
67
- name: parts[i],
68
- path: dirPath,
69
- type: "directory",
70
- children: [],
71
- };
72
- dirs.set(dirPath, dirNode);
73
-
74
- if (i === 0) {
75
- root.push(dirNode);
76
- } else {
77
- const parentPath = parts.slice(0, i).join("/");
78
- const parent = dirs.get(parentPath);
79
- parent?.children?.push(dirNode);
80
- }
81
- }
82
- }
83
-
84
- const fileNode: FileNode = {
85
- name: parts[parts.length - 1],
86
- path: filePath,
87
- type: "file",
88
- };
89
-
90
- if (parts.length === 1) {
91
- root.push(fileNode);
92
- } else {
93
- const parentPath = parts.slice(0, -1).join("/");
94
- const parent = dirs.get(parentPath);
95
- parent?.children?.push(fileNode);
96
- }
97
- }
98
-
99
- return root;
100
- }
101
-
102
- // ---------------------------------------------------------------------------
103
- // SVG Icons — inline for reliable rendering (no emoji issues)
104
- // ---------------------------------------------------------------------------
105
-
106
- /** Chevron right arrow for folder expand/collapse */
107
- function ChevronIcon({ expanded }: { expanded: boolean }) {
108
- return (
109
- <svg
110
- width="16"
111
- height="16"
112
- viewBox="0 0 16 16"
113
- fill="none"
114
- className="shrink-0 transition-transform duration-150"
115
- style={{ transform: expanded ? "rotate(90deg)" : "none" }}
116
- >
117
- <path
118
- d="M6 4l4 4-4 4"
119
- stroke="currentColor"
120
- strokeWidth="1.5"
121
- strokeLinecap="round"
122
- strokeLinejoin="round"
123
- />
124
- </svg>
125
- );
126
- }
127
-
128
- /** Folder icon (open or closed) */
129
- function FolderIcon({ open }: { open: boolean }) {
130
- if (open) {
131
- return (
132
- <svg
133
- width="16"
134
- height="16"
135
- viewBox="0 0 16 16"
136
- fill="none"
137
- className="shrink-0"
138
- >
139
- <path
140
- d="M1.5 3.5A1 1 0 012.5 2.5h3.172a1 1 0 01.707.293L7.5 3.914a1 1 0 00.707.293H13.5a1 1 0 011 1v1.293H3.207a1 1 0 00-.966.741L1.5 10.5V3.5z"
141
- fill="#dcb67a"
142
- />
143
- <path
144
- d="M2.241 7.241A1 1 0 013.207 6.5H14.5a1 1 0 01.966 1.259l-1.5 5.5A1 1 0 0113 14H2.5a1 1 0 01-1-1V8.5a1 1 0 01.741-.966l0-.293z"
145
- fill="#e8c87a"
146
- />
147
- </svg>
148
- );
149
- }
150
- return (
151
- <svg
152
- width="16"
153
- height="16"
154
- viewBox="0 0 16 16"
155
- fill="none"
156
- className="shrink-0"
157
- >
158
- <path
159
- d="M1.5 3A1.5 1.5 0 013 1.5h3.172a1.5 1.5 0 011.06.44L8.354 3.06a.5.5 0 00.354.147H13A1.5 1.5 0 0114.5 4.707V12A1.5 1.5 0 0113 13.5H3A1.5 1.5 0 011.5 12V3z"
160
- fill="#dcb67a"
161
- />
162
- </svg>
163
- );
164
- }
165
-
166
- // ---------------------------------------------------------------------------
167
- // Per-extension file icons — compact 16x16 SVGs with distinct shapes/labels.
168
- // Inspired by VS Code's Seti/Material icon themes.
169
- // ---------------------------------------------------------------------------
170
-
171
- /** SVG wrapper — common props for all file icons */
172
- function IconSvg({
173
- children,
174
- className = "",
175
- }: {
176
- children: React.ReactNode;
177
- className?: string;
178
- }) {
179
- return (
180
- <svg
181
- width="16"
182
- height="16"
183
- viewBox="0 0 16 16"
184
- fill="none"
185
- className={`shrink-0 ${className}`}
186
- >
187
- {children}
188
- </svg>
189
- );
190
- }
191
-
192
- /** Base document shape used by most file icons */
193
- function DocShape({ color }: { color: string }) {
194
- return (
195
- <>
196
- <path
197
- d="M4 1.5h5.5L13 5v9.5a1 1 0 01-1 1H4a1 1 0 01-1-1v-13a1 1 0 011-1z"
198
- fill={color}
199
- opacity="0.15"
200
- />
201
- <path
202
- d="M4 1.5h5.5L13 5v9.5a1 1 0 01-1 1H4a1 1 0 01-1-1v-13a1 1 0 011-1z"
203
- stroke={color}
204
- strokeWidth="0.8"
205
- />
206
- <path d="M9.5 1.5V5H13" stroke={color} strokeWidth="0.8" />
207
- </>
208
- );
209
- }
210
-
211
- /** Label text centered on the document icon */
212
- function DocLabel({
213
- label,
214
- color,
215
- y = "11.5",
216
- fontSize = "5",
217
- }: {
218
- label: string;
219
- color: string;
220
- y?: string;
221
- fontSize?: string;
222
- }) {
223
- return (
224
- <text
225
- x="8"
226
- y={y}
227
- textAnchor="middle"
228
- fill={color}
229
- fontSize={fontSize}
230
- fontWeight="700"
231
- fontFamily="Arial, sans-serif"
232
- >
233
- {label}
234
- </text>
235
- );
236
- }
237
-
238
- /** JavaScript (.js) */
239
- function JsIcon() {
240
- return (
241
- <IconSvg>
242
- <DocShape color="#f0db4f" />
243
- <DocLabel label="JS" color="#f0db4f" />
244
- </IconSvg>
245
- );
246
- }
247
-
248
- /** JSX (.jsx) */
249
- function JsxIcon() {
250
- return (
251
- <IconSvg>
252
- <DocShape color="#61dafb" />
253
- <DocLabel label="JSX" color="#61dafb" fontSize="4.5" />
254
- </IconSvg>
255
- );
256
- }
257
-
258
- /** TypeScript (.ts) */
259
- function TsIcon() {
260
- return (
261
- <IconSvg>
262
- <DocShape color="#3178c6" />
263
- <DocLabel label="TS" color="#3178c6" />
264
- </IconSvg>
265
- );
266
- }
267
-
268
- /** TSX (.tsx) */
269
- function TsxIcon() {
270
- return (
271
- <IconSvg>
272
- <DocShape color="#3178c6" />
273
- <DocLabel label="TSX" color="#61dafb" fontSize="4.5" />
274
- </IconSvg>
275
- );
276
- }
277
-
278
- /** JSON (.json) */
279
- function JsonIcon() {
280
- return (
281
- <IconSvg>
282
- <DocShape color="#a8b065" />
283
- <text
284
- x="8"
285
- y="11"
286
- textAnchor="middle"
287
- fill="#a8b065"
288
- fontSize="7"
289
- fontWeight="700"
290
- fontFamily="Arial, sans-serif"
291
- >
292
- {"{}"}
293
- </text>
294
- </IconSvg>
295
- );
296
- }
297
-
298
- /** HTML (.html) */
299
- function HtmlIcon() {
300
- return (
301
- <IconSvg>
302
- <DocShape color="#e44d26" />
303
- <text
304
- x="8"
305
- y="11.5"
306
- textAnchor="middle"
307
- fill="#e44d26"
308
- fontSize="4.5"
309
- fontWeight="700"
310
- fontFamily="Arial, sans-serif"
311
- >
312
- {"</>"}
313
- </text>
314
- </IconSvg>
315
- );
316
- }
317
-
318
- /** CSS (.css) */
319
- function CssIcon() {
320
- return (
321
- <IconSvg>
322
- <DocShape color="#264de4" />
323
- <DocLabel label="#" color="#264de4" fontSize="7" />
324
- </IconSvg>
325
- );
326
- }
327
-
328
- /** SCSS (.scss) */
329
- function ScssIcon() {
330
- return (
331
- <IconSvg>
332
- <DocShape color="#cd6799" />
333
- <DocLabel label="S" color="#cd6799" fontSize="7" />
334
- </IconSvg>
335
- );
336
- }
337
-
338
- /** Markdown (.md) */
339
- function MdIcon() {
340
- return (
341
- <IconSvg>
342
- <DocShape color="#9da5b4" />
343
- <DocLabel label="M↓" color="#9da5b4" fontSize="4.5" />
344
- </IconSvg>
345
- );
346
- }
347
-
348
- /** Python (.py) */
349
- function PyIcon() {
350
- return (
351
- <IconSvg>
352
- <DocShape color="#3776ab" />
353
- <DocLabel label="PY" color="#3776ab" />
354
- </IconSvg>
355
- );
356
- }
357
-
358
- /** Ruby (.rb) */
359
- function RbIcon() {
360
- return (
361
- <IconSvg>
362
- <DocShape color="#cc342d" />
363
- <text
364
- x="8"
365
- y="11.5"
366
- textAnchor="middle"
367
- fill="#cc342d"
368
- fontSize="6"
369
- fontWeight="700"
370
- fontFamily="Arial, sans-serif"
371
- >
372
- {"◆"}
373
- </text>
374
- </IconSvg>
375
- );
376
- }
377
-
378
- /** Go (.go) */
379
- function GoIcon() {
380
- return (
381
- <IconSvg>
382
- <DocShape color="#00add8" />
383
- <DocLabel label="GO" color="#00add8" />
384
- </IconSvg>
385
- );
386
- }
387
-
388
- /** Rust (.rs) */
389
- function RsIcon() {
390
- return (
391
- <IconSvg>
392
- <DocShape color="#dea584" />
393
- <DocLabel label="RS" color="#dea584" />
394
- </IconSvg>
395
- );
396
- }
397
-
398
- /** YAML (.yml, .yaml) */
399
- function YmlIcon() {
400
- return (
401
- <IconSvg>
402
- <DocShape color="#cb171e" />
403
- <DocLabel label="YML" color="#cb171e" fontSize="4" />
404
- </IconSvg>
405
- );
406
- }
407
-
408
- /** Env (.env) */
409
- function EnvIcon() {
410
- return (
411
- <IconSvg>
412
- <DocShape color="#ecd53f" />
413
- <text
414
- x="8"
415
- y="11.5"
416
- textAnchor="middle"
417
- fill="#ecd53f"
418
- fontSize="6"
419
- fontWeight="700"
420
- fontFamily="Arial, sans-serif"
421
- >
422
- {"⚙"}
423
- </text>
424
- </IconSvg>
425
- );
426
- }
427
-
428
- /** Shell (.sh, .bash) */
429
- function ShIcon() {
430
- return (
431
- <IconSvg>
432
- <DocShape color="#89e051" />
433
- <DocLabel label="$_" color="#89e051" fontSize="5" />
434
- </IconSvg>
435
- );
436
- }
437
-
438
- /** SQL (.sql) */
439
- function SqlIcon() {
440
- return (
441
- <IconSvg>
442
- <DocShape color="#e38c00" />
443
- <DocLabel label="SQL" color="#e38c00" fontSize="4" />
444
- </IconSvg>
445
- );
446
- }
447
-
448
- /** SVG (.svg) */
449
- function SvgIcon() {
450
- return (
451
- <IconSvg>
452
- <DocShape color="#ffb13b" />
453
- <DocLabel label="SVG" color="#ffb13b" fontSize="4" />
454
- </IconSvg>
455
- );
456
- }
457
-
458
- /** Lock files (package-lock.json, yarn.lock, etc.) */
459
- function LockIcon() {
460
- return (
461
- <IconSvg>
462
- <DocShape color="#6b7280" />
463
- <text
464
- x="8"
465
- y="11.5"
466
- textAnchor="middle"
467
- fill="#6b7280"
468
- fontSize="6"
469
- fontWeight="700"
470
- fontFamily="Arial, sans-serif"
471
- >
472
- {"🔒"}
473
- </text>
474
- </IconSvg>
475
- );
476
- }
477
-
478
- /** Generic fallback file icon */
479
- function DefaultFileIcon() {
480
- return (
481
- <IconSvg>
482
- <DocShape color="#8c8c8c" />
483
- </IconSvg>
484
- );
485
- }
486
-
487
- /**
488
- * Map of file extensions to their icon components.
489
- * Falls back to DefaultFileIcon for unrecognized extensions.
490
- */
491
- const ICON_BY_EXT: Record<string, React.FC> = {
492
- js: JsIcon,
493
- mjs: JsIcon,
494
- cjs: JsIcon,
495
- jsx: JsxIcon,
496
- ts: TsIcon,
497
- mts: TsIcon,
498
- cts: TsIcon,
499
- tsx: TsxIcon,
500
- json: JsonIcon,
501
- html: HtmlIcon,
502
- htm: HtmlIcon,
503
- css: CssIcon,
504
- scss: ScssIcon,
505
- sass: ScssIcon,
506
- less: ScssIcon,
507
- md: MdIcon,
508
- mdx: MdIcon,
509
- py: PyIcon,
510
- rb: RbIcon,
511
- go: GoIcon,
512
- rs: RsIcon,
513
- yml: YmlIcon,
514
- yaml: YmlIcon,
515
- env: EnvIcon,
516
- sh: ShIcon,
517
- bash: ShIcon,
518
- zsh: ShIcon,
519
- sql: SqlIcon,
520
- svg: SvgIcon,
521
- lock: LockIcon,
522
- };
523
-
524
- /**
525
- * Special filename overrides — some files are identified by their full name
526
- * rather than extension (e.g., Dockerfile, .gitignore).
527
- */
528
- const ICON_BY_NAME: Record<string, React.FC> = {
529
- "package-lock.json": LockIcon,
530
- "yarn.lock": LockIcon,
531
- "pnpm-lock.yaml": LockIcon,
532
- };
533
-
534
- /** Resolve the correct icon component for a filename */
535
- function FileIcon({ name }: { name: string }) {
536
- // Check full filename first (for special cases)
537
- const lowerName = name.toLowerCase();
538
- const ByName = ICON_BY_NAME[lowerName];
539
- if (ByName) return <ByName />;
540
-
541
- // Then check by extension
542
- const ext = lowerName.split(".").pop() || "";
543
- // Handle dotfiles like .env, .gitignore — use the part after the dot
544
- const dotfileExt = lowerName.startsWith(".") ? lowerName.slice(1) : "";
545
- const Icon = ICON_BY_EXT[ext] || ICON_BY_EXT[dotfileExt] || DefaultFileIcon;
546
- return <Icon />;
547
- }
548
-
549
- // ---------------------------------------------------------------------------
550
- // Components
551
- // ---------------------------------------------------------------------------
552
-
553
- /**
554
- * FileTree component — renders a collapsible tree of files and folders.
555
- * Shows change status indicators (N/M/D) next to modified files.
556
- */
557
- export function FileTree({
558
- files,
559
- selectedFile,
560
- onSelectFile,
561
- fileChanges,
562
- }: FileTreeProps) {
563
- return (
564
- <div className="h-full overflow-auto bg-sb-sidebar text-sm select-none overscroll-contain">
565
- <div className="px-3 py-2 text-[11px] font-semibold text-sb-text-muted uppercase tracking-wider border-b border-sb-border">
566
- Explorer
567
- </div>
568
- <div className="py-1">
569
- {files.map((node) => (
570
- <TreeNode
571
- key={node.path}
572
- node={node}
573
- depth={0}
574
- selectedFile={selectedFile}
575
- onSelectFile={onSelectFile}
576
- fileChanges={fileChanges}
577
- />
578
- ))}
579
- </div>
580
- </div>
581
- );
582
- }
583
-
584
- /** Individual tree node (file or folder) */
585
- function TreeNode({
586
- node,
587
- depth,
588
- selectedFile,
589
- onSelectFile,
590
- fileChanges,
591
- }: {
592
- node: FileNode;
593
- depth: number;
594
- selectedFile: string | null;
595
- onSelectFile: (path: string) => void;
596
- fileChanges?: Record<string, FileChangeStatus>;
597
- }) {
598
- const [expanded, setExpanded] = useState(depth < 2);
599
-
600
- const isSelected = node.path === selectedFile;
601
- const paddingLeft = 8 + depth * 16;
602
- const changeStatus: FileChangeStatus =
603
- fileChanges?.[node.path] ?? "unchanged";
604
-
605
- if (node.type === "directory") {
606
- // Check if any child has changes (to show a subtle indicator on folders)
607
- const hasChangedChild =
608
- fileChanges &&
609
- node.children?.some((c) => {
610
- if (fileChanges[c.path]) return true;
611
- // For directories, check recursively by prefix
612
- if (c.type === "directory") {
613
- return Object.keys(fileChanges).some(
614
- (p) => p.startsWith(c.path + "/") && fileChanges[p] !== "unchanged",
615
- );
616
- }
617
- return false;
618
- });
619
-
620
- return (
621
- <div>
622
- <button
623
- className={`w-full flex items-center gap-1 py-[3px] text-left hover:bg-sb-bg-hover transition-colors ${
624
- isSelected ? "bg-sb-bg-active text-sb-text-active" : "text-sb-text"
625
- }`}
626
- style={{ paddingLeft }}
627
- onClick={() => setExpanded(!expanded)}
628
- >
629
- <ChevronIcon expanded={expanded} />
630
- <FolderIcon open={expanded} />
631
- <span className="truncate ml-0.5 flex-1">{node.name}</span>
632
- {hasChangedChild && (
633
- <span
634
- className="w-1.5 h-1.5 rounded-full shrink-0 mr-2"
635
- style={{ backgroundColor: "#fb923c" }}
636
- />
637
- )}
638
- </button>
639
- {expanded && node.children && (
640
- <div>
641
- {node.children.map((child) => (
642
- <TreeNode
643
- key={child.path}
644
- node={child}
645
- depth={depth + 1}
646
- selectedFile={selectedFile}
647
- onSelectFile={onSelectFile}
648
- fileChanges={fileChanges}
649
- />
650
- ))}
651
- </div>
652
- )}
653
- </div>
654
- );
655
- }
656
-
657
- return (
658
- <button
659
- className={`w-full flex items-center gap-1 py-[3px] text-left hover:bg-sb-bg-hover transition-colors ${
660
- isSelected ? "bg-sb-bg-active text-sb-text-active" : "text-sb-text"
661
- }`}
662
- style={{ paddingLeft: paddingLeft + 20 }}
663
- onClick={() => onSelectFile(node.path)}
664
- >
665
- <FileIcon name={node.name} />
666
- <span className="truncate ml-0.5 flex-1">{node.name}</span>
667
- {changeStatus !== "unchanged" && (
668
- <span
669
- className="text-[9px] font-bold shrink-0 mr-2"
670
- style={{ color: STATUS_COLORS[changeStatus] }}
671
- title={changeStatus}
672
- >
673
- {STATUS_LETTERS[changeStatus]}
674
- </span>
675
- )}
676
- </button>
677
- );
678
- }