create-interview-cockpit 0.16.0 → 0.17.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/package.json +1 -1
- package/template/client/package-lock.json +75 -2
- package/template/client/package.json +3 -1
- package/template/client/src/browserSecurityTemplates.ts +75 -72
- package/template/client/src/components/CodeRunnerModal.tsx +406 -22
- package/template/client/src/reactLab.ts +374 -152
- package/template/cockpit.json +1 -1
- package/template/server/src/index.ts +134 -47
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
} from "lucide-react";
|
|
36
36
|
import { useStore } from "../store";
|
|
37
37
|
import Editor from "react-simple-code-editor";
|
|
38
|
+
import MonacoEditorLib from "@monaco-editor/react";
|
|
39
|
+
import type { OnMount, BeforeMount, Monaco } from "@monaco-editor/react";
|
|
40
|
+
import { setupTypeAcquisition } from "@typescript/ata";
|
|
41
|
+
import ts from "typescript";
|
|
38
42
|
import Prism from "prismjs";
|
|
39
43
|
import "prismjs/components/prism-clike";
|
|
40
44
|
import "prismjs/components/prism-javascript";
|
|
@@ -361,6 +365,269 @@ function SyntaxEditor({
|
|
|
361
365
|
);
|
|
362
366
|
}
|
|
363
367
|
|
|
368
|
+
// Monaco Editor wrapper — same prop interface as SyntaxEditor.
|
|
369
|
+
// Provides full VS Code intellisense/autocomplete while preserving
|
|
370
|
+
// all existing run/output/chat functionality.
|
|
371
|
+
// ATA (Automatic Type Acquisition) — watches imports as you type and fetches
|
|
372
|
+
// .d.ts files from the npm CDN, then feeds them to Monaco's TypeScript worker.
|
|
373
|
+
// This gives framework-specific intellisense (Next.js, React, Express, etc.)
|
|
374
|
+
// identical to what VS Code shows, without any server involvement.
|
|
375
|
+
function useATA(monacoRef: React.MutableRefObject<Monaco | null>) {
|
|
376
|
+
const ataRef = useRef<ReturnType<typeof setupTypeAcquisition> | null>(null);
|
|
377
|
+
|
|
378
|
+
const initATA = useCallback(() => {
|
|
379
|
+
const monaco = monacoRef.current;
|
|
380
|
+
if (!monaco || ataRef.current) return;
|
|
381
|
+
|
|
382
|
+
ataRef.current = setupTypeAcquisition({
|
|
383
|
+
projectName: "interview-cockpit",
|
|
384
|
+
typescript: ts,
|
|
385
|
+
logger: console,
|
|
386
|
+
delegate: {
|
|
387
|
+
receivedFile: (code: string, path: string) => {
|
|
388
|
+
// Add each fetched .d.ts into Monaco's virtual file system
|
|
389
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
390
|
+
const tsLang = monaco.languages.typescript as any;
|
|
391
|
+
tsLang.typescriptDefaults.addExtraLib(code, `file://${path}`);
|
|
392
|
+
tsLang.javascriptDefaults.addExtraLib(code, `file://${path}`);
|
|
393
|
+
},
|
|
394
|
+
progress: (_downloaded: number, _total: number) => {},
|
|
395
|
+
started: () => {},
|
|
396
|
+
finished: (_vfs: Map<string, string>) => {},
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
}, [monacoRef]);
|
|
400
|
+
|
|
401
|
+
// Call this whenever editor code changes so ATA can scan new imports
|
|
402
|
+
const acquireTypes = useCallback((code: string) => {
|
|
403
|
+
ataRef.current?.(code);
|
|
404
|
+
}, []);
|
|
405
|
+
|
|
406
|
+
return { initATA, acquireTypes };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function MonacoEditorWrapper({
|
|
410
|
+
value,
|
|
411
|
+
onChange,
|
|
412
|
+
onCtrlEnter,
|
|
413
|
+
language,
|
|
414
|
+
placeholder,
|
|
415
|
+
autoFocus = false,
|
|
416
|
+
fontSize = "12px",
|
|
417
|
+
}: {
|
|
418
|
+
value: string;
|
|
419
|
+
onChange: (val: string) => void;
|
|
420
|
+
onCtrlEnter?: () => void;
|
|
421
|
+
language: string;
|
|
422
|
+
placeholder?: string;
|
|
423
|
+
autoFocus?: boolean;
|
|
424
|
+
fontSize?: string;
|
|
425
|
+
focusRingClass?: string;
|
|
426
|
+
}) {
|
|
427
|
+
const fontSizePx = parseInt(fontSize) || 12;
|
|
428
|
+
const monacoRef = useRef<Monaco | null>(null);
|
|
429
|
+
const { initATA, acquireTypes } = useATA(monacoRef);
|
|
430
|
+
|
|
431
|
+
const handleBeforeMount: BeforeMount = (monaco) => {
|
|
432
|
+
monacoRef.current = monaco;
|
|
433
|
+
|
|
434
|
+
// Configure TypeScript compiler options to match a modern TS project
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
436
|
+
const tsLang = monaco.languages.typescript as any;
|
|
437
|
+
const compilerOptions = {
|
|
438
|
+
target: tsLang.ScriptTarget.ESNext,
|
|
439
|
+
module: tsLang.ModuleKind.ESNext,
|
|
440
|
+
moduleResolution: tsLang.ModuleResolutionKind.Bundler,
|
|
441
|
+
jsx: tsLang.JsxEmit.ReactJSX,
|
|
442
|
+
strict: true,
|
|
443
|
+
esModuleInterop: true,
|
|
444
|
+
allowSyntheticDefaultImports: true,
|
|
445
|
+
allowNonTsExtensions: true,
|
|
446
|
+
};
|
|
447
|
+
tsLang.typescriptDefaults.setCompilerOptions(compilerOptions);
|
|
448
|
+
tsLang.javascriptDefaults.setCompilerOptions(compilerOptions);
|
|
449
|
+
|
|
450
|
+
// Suppress "Cannot find module" errors — packages aren't installed in the sandbox
|
|
451
|
+
const diagnosticsOptions = {
|
|
452
|
+
noSemanticValidation: false,
|
|
453
|
+
noSyntaxValidation: false,
|
|
454
|
+
diagnosticCodesToIgnore: [2792, 2307],
|
|
455
|
+
};
|
|
456
|
+
tsLang.typescriptDefaults.setDiagnosticsOptions(diagnosticsOptions);
|
|
457
|
+
tsLang.javascriptDefaults.setDiagnosticsOptions(diagnosticsOptions);
|
|
458
|
+
|
|
459
|
+
// Allow importing from node_modules type definitions
|
|
460
|
+
tsLang.typescriptDefaults.setEagerModelSync(true);
|
|
461
|
+
tsLang.javascriptDefaults.setEagerModelSync(true);
|
|
462
|
+
|
|
463
|
+
// Stub Node.js globals (process, Buffer, etc.) so sandbox code using them doesn't error
|
|
464
|
+
const nodeGlobals = `
|
|
465
|
+
declare var process: {
|
|
466
|
+
env: Record<string, string | undefined> & {
|
|
467
|
+
NODE_ENV: 'development' | 'production' | 'test';
|
|
468
|
+
REMOTE_PORT?: string;
|
|
469
|
+
PORT?: string;
|
|
470
|
+
HOST?: string;
|
|
471
|
+
[key: string]: string | undefined;
|
|
472
|
+
};
|
|
473
|
+
argv: string[];
|
|
474
|
+
cwd(): string;
|
|
475
|
+
exit(code?: number): never;
|
|
476
|
+
platform: string;
|
|
477
|
+
version: string;
|
|
478
|
+
versions: Record<string, string>;
|
|
479
|
+
};
|
|
480
|
+
declare var Buffer: {
|
|
481
|
+
from(data: string | ArrayBuffer | ArrayBufferView, encoding?: string): { toString(encoding?: string): string };
|
|
482
|
+
alloc(size: number): Uint8Array;
|
|
483
|
+
isBuffer(obj: unknown): boolean;
|
|
484
|
+
};
|
|
485
|
+
declare var __dirname: string;
|
|
486
|
+
declare var __filename: string;
|
|
487
|
+
`;
|
|
488
|
+
tsLang.typescriptDefaults.addExtraLib(
|
|
489
|
+
nodeGlobals,
|
|
490
|
+
"file:///node_modules/@types/node/globals.d.ts",
|
|
491
|
+
);
|
|
492
|
+
tsLang.javascriptDefaults.addExtraLib(
|
|
493
|
+
nodeGlobals,
|
|
494
|
+
"file:///node_modules/@types/node/globals.d.ts",
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
// Pre-seed Next.js type stubs. ATA cannot resolve `next` because Next uses a
|
|
498
|
+
// complex package.json exports map that unpkg / CDN ATA doesn't traverse.
|
|
499
|
+
const nextTypes = `
|
|
500
|
+
declare module 'next' {
|
|
501
|
+
export type NextConfig = {
|
|
502
|
+
reactStrictMode?: boolean;
|
|
503
|
+
swcMinify?: boolean;
|
|
504
|
+
output?: 'standalone' | 'export';
|
|
505
|
+
compress?: boolean;
|
|
506
|
+
poweredByHeader?: boolean;
|
|
507
|
+
distDir?: string;
|
|
508
|
+
assetPrefix?: string;
|
|
509
|
+
basePath?: string;
|
|
510
|
+
trailingSlash?: boolean;
|
|
511
|
+
pageExtensions?: string[];
|
|
512
|
+
env?: Record<string, string>;
|
|
513
|
+
images?: {
|
|
514
|
+
domains?: string[];
|
|
515
|
+
remotePatterns?: { protocol?: 'http' | 'https'; hostname: string; port?: string; pathname?: string }[];
|
|
516
|
+
formats?: ('image/avif' | 'image/webp')[];
|
|
517
|
+
unoptimized?: boolean;
|
|
518
|
+
};
|
|
519
|
+
experimental?: { serverActions?: boolean | { allowedOrigins?: string[] }; [key: string]: unknown };
|
|
520
|
+
webpack?: (config: any, options: { buildId: string; dev: boolean; isServer: boolean; defaultLoaders: any; webpack: any }) => any;
|
|
521
|
+
rewrites?: () => Promise<{ source: string; destination: string }[] | { beforeFiles?: any[]; afterFiles?: any[]; fallback?: any[] }>;
|
|
522
|
+
redirects?: () => Promise<{ source: string; destination: string; permanent: boolean }[]>;
|
|
523
|
+
headers?: () => Promise<{ source: string; headers: { key: string; value: string }[] }[]>;
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
declare module 'next/navigation' {
|
|
527
|
+
export function useRouter(): { push(href: string): void; replace(href: string): void; back(): void; forward(): void; refresh(): void; prefetch(href: string): void };
|
|
528
|
+
export function usePathname(): string;
|
|
529
|
+
export function useSearchParams(): URLSearchParams;
|
|
530
|
+
export function useParams<T extends Record<string, string | string[]> = Record<string, string | string[]>>(): T;
|
|
531
|
+
export function redirect(url: string): never;
|
|
532
|
+
export function notFound(): never;
|
|
533
|
+
}
|
|
534
|
+
declare module 'next/link' {
|
|
535
|
+
import * as React from 'react';
|
|
536
|
+
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> { href: string | { pathname?: string; query?: Record<string, string> }; replace?: boolean; prefetch?: boolean; }
|
|
537
|
+
const Link: React.FC<LinkProps>;
|
|
538
|
+
export default Link;
|
|
539
|
+
}
|
|
540
|
+
declare module 'next/image' {
|
|
541
|
+
import * as React from 'react';
|
|
542
|
+
export interface ImageProps { src: string; alt: string; width?: number; height?: number; fill?: boolean; priority?: boolean; quality?: number; className?: string; style?: React.CSSProperties; }
|
|
543
|
+
const Image: React.FC<ImageProps>;
|
|
544
|
+
export default Image;
|
|
545
|
+
}
|
|
546
|
+
declare module 'next/font/google' {
|
|
547
|
+
export interface FontOptions { subsets?: string[]; weight?: string | string[]; style?: string | string[]; variable?: string; display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional'; }
|
|
548
|
+
export type FontResult = { className: string; style: { fontFamily: string; fontWeight?: number; fontStyle?: string }; variable: string };
|
|
549
|
+
export function Inter(options?: FontOptions): FontResult;
|
|
550
|
+
export function Roboto(options?: FontOptions): FontResult;
|
|
551
|
+
export function Geist(options?: FontOptions): FontResult;
|
|
552
|
+
export function Geist_Mono(options?: FontOptions): FontResult;
|
|
553
|
+
}
|
|
554
|
+
declare module 'next/server' {
|
|
555
|
+
export class NextResponse extends Response {
|
|
556
|
+
static json<T>(data: T, init?: ResponseInit): NextResponse;
|
|
557
|
+
static redirect(url: string | URL, status?: number): NextResponse;
|
|
558
|
+
static next(): NextResponse;
|
|
559
|
+
}
|
|
560
|
+
export class NextRequest extends Request {
|
|
561
|
+
readonly nextUrl: URL;
|
|
562
|
+
readonly cookies: { get(name: string): { name: string; value: string } | undefined; getAll(): { name: string; value: string }[] };
|
|
563
|
+
}
|
|
564
|
+
export type NextMiddleware = (request: NextRequest) => Response | NextResponse | void | Promise<Response | NextResponse | void>;
|
|
565
|
+
}
|
|
566
|
+
`;
|
|
567
|
+
tsLang.typescriptDefaults.addExtraLib(
|
|
568
|
+
nextTypes,
|
|
569
|
+
"file:///node_modules/next/index.d.ts",
|
|
570
|
+
);
|
|
571
|
+
tsLang.javascriptDefaults.addExtraLib(
|
|
572
|
+
nextTypes,
|
|
573
|
+
"file:///node_modules/next/index.d.ts",
|
|
574
|
+
);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const handleMount: OnMount = (editor, monaco) => {
|
|
578
|
+
monacoRef.current = monaco;
|
|
579
|
+
initATA();
|
|
580
|
+
// Kick off an initial scan for any imports already in the file
|
|
581
|
+
acquireTypes(value);
|
|
582
|
+
if (autoFocus) editor.focus();
|
|
583
|
+
if (onCtrlEnter) {
|
|
584
|
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () =>
|
|
585
|
+
onCtrlEnter(),
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// Re-scan for new imports whenever code changes
|
|
591
|
+
useEffect(() => {
|
|
592
|
+
acquireTypes(value);
|
|
593
|
+
}, [value, acquireTypes]);
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<div className="absolute inset-0">
|
|
597
|
+
<MonacoEditorLib
|
|
598
|
+
height="100%"
|
|
599
|
+
width="100%"
|
|
600
|
+
language={language === "typescript" ? "typescript" : "javascript"}
|
|
601
|
+
theme="vs-dark"
|
|
602
|
+
value={value}
|
|
603
|
+
onChange={(val) => onChange(val ?? "")}
|
|
604
|
+
beforeMount={handleBeforeMount}
|
|
605
|
+
onMount={handleMount}
|
|
606
|
+
options={{
|
|
607
|
+
fontSize: fontSizePx,
|
|
608
|
+
fontFamily: EDITOR_FONT,
|
|
609
|
+
minimap: { enabled: false },
|
|
610
|
+
scrollBeyondLastLine: false,
|
|
611
|
+
lineNumbers: "on",
|
|
612
|
+
glyphMargin: false,
|
|
613
|
+
folding: false,
|
|
614
|
+
lineDecorationsWidth: 4,
|
|
615
|
+
lineNumbersMinChars: 3,
|
|
616
|
+
renderLineHighlight: "line",
|
|
617
|
+
overviewRulerLanes: 0,
|
|
618
|
+
hideCursorInOverviewRuler: true,
|
|
619
|
+
overviewRulerBorder: false,
|
|
620
|
+
scrollbar: { vertical: "auto", horizontal: "auto" },
|
|
621
|
+
padding: { top: 8, bottom: 8 },
|
|
622
|
+
wordWrap: "off",
|
|
623
|
+
automaticLayout: true,
|
|
624
|
+
placeholder,
|
|
625
|
+
}}
|
|
626
|
+
/>
|
|
627
|
+
</div>
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
364
631
|
interface FileTreeNode {
|
|
365
632
|
name: string;
|
|
366
633
|
path: string;
|
|
@@ -674,6 +941,9 @@ export default function CodeRunnerModal() {
|
|
|
674
941
|
runnerInitialFileId ?? null,
|
|
675
942
|
);
|
|
676
943
|
|
|
944
|
+
// ── Editor mode ───────────────────────────────────────────
|
|
945
|
+
const [useMonacoEditor, setUseMonacoEditor] = useState(false);
|
|
946
|
+
|
|
677
947
|
// ── Sandbox state ─────────────────────────────────────────
|
|
678
948
|
const [mode, setMode] = useState<"script" | "sandbox">("script");
|
|
679
949
|
const [serverCode, setServerCode] = useState(DEFAULT_SERVER_CODE);
|
|
@@ -698,6 +968,9 @@ export default function CodeRunnerModal() {
|
|
|
698
968
|
const [reactPreviewSrc, setReactPreviewSrc] = useState<string | null>(null);
|
|
699
969
|
const [reactAddingFile, setReactAddingFile] = useState(false);
|
|
700
970
|
const [reactNewFileName, setReactNewFileName] = useState("");
|
|
971
|
+
// Explorer sidebar width (resizable via drag handle)
|
|
972
|
+
const [explorerWidth, setExplorerWidth] = useState(144);
|
|
973
|
+
const explorerDragStart = useRef<{ x: number; w: number } | null>(null);
|
|
701
974
|
// Folders that are collapsed in the Next.js file tree sidebar
|
|
702
975
|
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(
|
|
703
976
|
new Set(),
|
|
@@ -3464,6 +3737,26 @@ export default function CodeRunnerModal() {
|
|
|
3464
3737
|
</>
|
|
3465
3738
|
)}
|
|
3466
3739
|
|
|
3740
|
+
{/* Monaco toggle */}
|
|
3741
|
+
<button
|
|
3742
|
+
type="button"
|
|
3743
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
3744
|
+
onClick={() => setUseMonacoEditor((v) => !v)}
|
|
3745
|
+
className={`flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-mono font-medium transition-colors shrink-0 ${
|
|
3746
|
+
useMonacoEditor
|
|
3747
|
+
? "bg-violet-600/30 text-violet-300 ring-1 ring-violet-500/40"
|
|
3748
|
+
: "text-slate-500 hover:text-slate-300 hover:bg-slate-700"
|
|
3749
|
+
}`}
|
|
3750
|
+
title={
|
|
3751
|
+
useMonacoEditor
|
|
3752
|
+
? "Switch to simple editor"
|
|
3753
|
+
: "Switch to Monaco (VS Code intellisense)"
|
|
3754
|
+
}
|
|
3755
|
+
>
|
|
3756
|
+
<Code2 className="w-3 h-3" />
|
|
3757
|
+
Monaco
|
|
3758
|
+
</button>
|
|
3759
|
+
|
|
3467
3760
|
<button
|
|
3468
3761
|
onMouseDown={(e) => e.stopPropagation()}
|
|
3469
3762
|
onClick={toggleMax}
|
|
@@ -3491,16 +3784,28 @@ export default function CodeRunnerModal() {
|
|
|
3491
3784
|
<div className="flex flex-col flex-1 overflow-hidden">
|
|
3492
3785
|
{/* Editor pane */}
|
|
3493
3786
|
<div className="flex-1 min-h-0 relative">
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3787
|
+
{useMonacoEditor ? (
|
|
3788
|
+
<MonacoEditorWrapper
|
|
3789
|
+
value={code}
|
|
3790
|
+
onChange={setCode}
|
|
3791
|
+
onCtrlEnter={() => void runCode()}
|
|
3792
|
+
language={lang}
|
|
3793
|
+
autoFocus
|
|
3794
|
+
fontSize="13px"
|
|
3795
|
+
placeholder={`// TypeScript / JavaScript\n// Ctrl+Enter to run\n\nconst res = await fetch('https://jsonplaceholder.typicode.com/todos/1');\nconst data = await res.json();\nconsole.log(data);`}
|
|
3796
|
+
/>
|
|
3797
|
+
) : (
|
|
3798
|
+
<SyntaxEditor
|
|
3799
|
+
value={code}
|
|
3800
|
+
onChange={setCode}
|
|
3801
|
+
onCtrlEnter={() => void runCode()}
|
|
3802
|
+
language={lang}
|
|
3803
|
+
autoFocus
|
|
3804
|
+
fontSize="13px"
|
|
3805
|
+
focusRingClass="ring-violet-500/40"
|
|
3806
|
+
placeholder={`// TypeScript / JavaScript\n// Ctrl+Enter to run\n\nconst res = await fetch('https://jsonplaceholder.typicode.com/todos/1');\nconst data = await res.json();\nconsole.log(data);`}
|
|
3807
|
+
/>
|
|
3808
|
+
)}
|
|
3504
3809
|
</div>
|
|
3505
3810
|
|
|
3506
3811
|
{/* Divider */}
|
|
@@ -3655,17 +3960,30 @@ export default function CodeRunnerModal() {
|
|
|
3655
3960
|
)}
|
|
3656
3961
|
</div>
|
|
3657
3962
|
<div className="flex-1 min-h-0 relative">
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3963
|
+
{useMonacoEditor ? (
|
|
3964
|
+
<MonacoEditorWrapper
|
|
3965
|
+
value={serverCode}
|
|
3966
|
+
onChange={setServerCode}
|
|
3967
|
+
onCtrlEnter={() => void startServer()}
|
|
3968
|
+
language={serverLang}
|
|
3969
|
+
fontSize="12px"
|
|
3970
|
+
placeholder={
|
|
3971
|
+
"import express from 'express';\n// process.env.PORT is auto-injected\n// Ctrl+Enter to start"
|
|
3972
|
+
}
|
|
3973
|
+
/>
|
|
3974
|
+
) : (
|
|
3975
|
+
<SyntaxEditor
|
|
3976
|
+
value={serverCode}
|
|
3977
|
+
onChange={setServerCode}
|
|
3978
|
+
onCtrlEnter={() => void startServer()}
|
|
3979
|
+
language={serverLang}
|
|
3980
|
+
fontSize="12px"
|
|
3981
|
+
focusRingClass="ring-emerald-500/30"
|
|
3982
|
+
placeholder={
|
|
3983
|
+
"import express from 'express';\n// process.env.PORT is auto-injected\n// Ctrl+Enter to start"
|
|
3984
|
+
}
|
|
3985
|
+
/>
|
|
3986
|
+
)}
|
|
3669
3987
|
</div>
|
|
3670
3988
|
</div>
|
|
3671
3989
|
)}
|
|
@@ -4057,7 +4375,10 @@ export default function CodeRunnerModal() {
|
|
|
4057
4375
|
>
|
|
4058
4376
|
{/* ── VS Code-style file tree sidebar ── */}
|
|
4059
4377
|
{usesClientExplorer && (
|
|
4060
|
-
<div
|
|
4378
|
+
<div
|
|
4379
|
+
className="shrink-0 flex flex-col border-r border-slate-700 bg-slate-900/60 overflow-y-auto relative"
|
|
4380
|
+
style={{ width: explorerWidth }}
|
|
4381
|
+
>
|
|
4061
4382
|
{/* Sidebar header */}
|
|
4062
4383
|
<div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-700/60">
|
|
4063
4384
|
<span className="text-[9px] uppercase tracking-widest text-slate-500 font-semibold select-none">
|
|
@@ -4301,6 +4622,37 @@ export default function CodeRunnerModal() {
|
|
|
4301
4622
|
return renderNode(tree);
|
|
4302
4623
|
})()}
|
|
4303
4624
|
</div>
|
|
4625
|
+
{/* Drag handle — inside the relative explorer container */}
|
|
4626
|
+
<div
|
|
4627
|
+
className="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-cyan-500/40 active:bg-cyan-500/60 transition-colors z-10"
|
|
4628
|
+
onMouseDown={(e) => {
|
|
4629
|
+
e.preventDefault();
|
|
4630
|
+
explorerDragStart.current = {
|
|
4631
|
+
x: e.clientX,
|
|
4632
|
+
w: explorerWidth,
|
|
4633
|
+
};
|
|
4634
|
+
const onMove = (ev: MouseEvent) => {
|
|
4635
|
+
if (!explorerDragStart.current) return;
|
|
4636
|
+
const newW = Math.max(
|
|
4637
|
+
100,
|
|
4638
|
+
Math.min(
|
|
4639
|
+
320,
|
|
4640
|
+
explorerDragStart.current.w +
|
|
4641
|
+
ev.clientX -
|
|
4642
|
+
explorerDragStart.current.x,
|
|
4643
|
+
),
|
|
4644
|
+
);
|
|
4645
|
+
setExplorerWidth(newW);
|
|
4646
|
+
};
|
|
4647
|
+
const onUp = () => {
|
|
4648
|
+
explorerDragStart.current = null;
|
|
4649
|
+
window.removeEventListener("mousemove", onMove);
|
|
4650
|
+
window.removeEventListener("mouseup", onUp);
|
|
4651
|
+
};
|
|
4652
|
+
window.addEventListener("mousemove", onMove);
|
|
4653
|
+
window.addEventListener("mouseup", onUp);
|
|
4654
|
+
}}
|
|
4655
|
+
/>
|
|
4304
4656
|
</div>
|
|
4305
4657
|
)}
|
|
4306
4658
|
|
|
@@ -4363,6 +4715,19 @@ export default function CodeRunnerModal() {
|
|
|
4363
4715
|
</div>
|
|
4364
4716
|
)}
|
|
4365
4717
|
</div>
|
|
4718
|
+
) : useMonacoEditor ? (
|
|
4719
|
+
<MonacoEditorWrapper
|
|
4720
|
+
value={clientCode}
|
|
4721
|
+
onChange={setClientCode}
|
|
4722
|
+
onCtrlEnter={() => {
|
|
4723
|
+
if (serverRunning) void runClient();
|
|
4724
|
+
}}
|
|
4725
|
+
language={clientLang}
|
|
4726
|
+
fontSize="12px"
|
|
4727
|
+
placeholder={
|
|
4728
|
+
"// SANDBOX_URL is injected automatically\n// Start the server first, then Ctrl+Enter to run"
|
|
4729
|
+
}
|
|
4730
|
+
/>
|
|
4366
4731
|
) : (
|
|
4367
4732
|
<SyntaxEditor
|
|
4368
4733
|
value={clientCode}
|
|
@@ -4400,6 +4765,25 @@ export default function CodeRunnerModal() {
|
|
|
4400
4765
|
)}
|
|
4401
4766
|
</div>
|
|
4402
4767
|
</div>
|
|
4768
|
+
) : useMonacoEditor ? (
|
|
4769
|
+
<MonacoEditorWrapper
|
|
4770
|
+
key={reactActiveFile}
|
|
4771
|
+
value={reactFiles[reactActiveFile] ?? ""}
|
|
4772
|
+
onChange={(val) =>
|
|
4773
|
+
setReactFiles((prev) => ({
|
|
4774
|
+
...prev,
|
|
4775
|
+
[reactActiveFile]: val,
|
|
4776
|
+
}))
|
|
4777
|
+
}
|
|
4778
|
+
language={
|
|
4779
|
+
reactActiveFile.endsWith(".ts") ||
|
|
4780
|
+
reactActiveFile.endsWith(".tsx")
|
|
4781
|
+
? "typescript"
|
|
4782
|
+
: "javascript"
|
|
4783
|
+
}
|
|
4784
|
+
fontSize="12px"
|
|
4785
|
+
placeholder={`// ${reactActiveFile}\n`}
|
|
4786
|
+
/>
|
|
4403
4787
|
) : (
|
|
4404
4788
|
<SyntaxEditor
|
|
4405
4789
|
key={reactActiveFile}
|