create-interview-cockpit 0.16.0 → 0.17.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/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 +504 -24
- 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,348 @@ 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
|
+
let monacoSandboxTypeLibsInjected = false;
|
|
410
|
+
|
|
411
|
+
function MonacoEditorWrapper({
|
|
412
|
+
value,
|
|
413
|
+
onChange,
|
|
414
|
+
onCtrlEnter,
|
|
415
|
+
language,
|
|
416
|
+
path,
|
|
417
|
+
placeholder,
|
|
418
|
+
autoFocus = false,
|
|
419
|
+
fontSize = "12px",
|
|
420
|
+
}: {
|
|
421
|
+
value: string;
|
|
422
|
+
onChange: (val: string) => void;
|
|
423
|
+
onCtrlEnter?: () => void;
|
|
424
|
+
language: string;
|
|
425
|
+
path?: string;
|
|
426
|
+
placeholder?: string;
|
|
427
|
+
autoFocus?: boolean;
|
|
428
|
+
fontSize?: string;
|
|
429
|
+
focusRingClass?: string;
|
|
430
|
+
}) {
|
|
431
|
+
const fontSizePx = parseInt(fontSize) || 12;
|
|
432
|
+
const monacoRef = useRef<Monaco | null>(null);
|
|
433
|
+
const { initATA, acquireTypes } = useATA(monacoRef);
|
|
434
|
+
|
|
435
|
+
const handleBeforeMount: BeforeMount = (monaco) => {
|
|
436
|
+
monacoRef.current = monaco;
|
|
437
|
+
|
|
438
|
+
// Configure TypeScript compiler options to match a modern TS project
|
|
439
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
440
|
+
const tsLang = monaco.languages.typescript as any;
|
|
441
|
+
const compilerOptions = {
|
|
442
|
+
target: tsLang.ScriptTarget.ESNext,
|
|
443
|
+
module: tsLang.ModuleKind.ESNext,
|
|
444
|
+
moduleResolution: tsLang.ModuleResolutionKind.Bundler,
|
|
445
|
+
// Prefer modern automatic JSX runtime; keep a fallback for older Monaco TS builds.
|
|
446
|
+
jsx: tsLang.JsxEmit.ReactJSX ?? tsLang.JsxEmit.Preserve,
|
|
447
|
+
jsxImportSource: "react",
|
|
448
|
+
allowJs: true,
|
|
449
|
+
strict: true,
|
|
450
|
+
esModuleInterop: true,
|
|
451
|
+
allowSyntheticDefaultImports: true,
|
|
452
|
+
allowNonTsExtensions: true,
|
|
453
|
+
};
|
|
454
|
+
tsLang.typescriptDefaults.setCompilerOptions(compilerOptions);
|
|
455
|
+
tsLang.javascriptDefaults.setCompilerOptions(compilerOptions);
|
|
456
|
+
|
|
457
|
+
// Suppress "Cannot find module" errors — packages aren't installed in the sandbox
|
|
458
|
+
const diagnosticsOptions = {
|
|
459
|
+
noSemanticValidation: false,
|
|
460
|
+
noSyntaxValidation: false,
|
|
461
|
+
diagnosticCodesToIgnore: [2792, 2307],
|
|
462
|
+
};
|
|
463
|
+
tsLang.typescriptDefaults.setDiagnosticsOptions(diagnosticsOptions);
|
|
464
|
+
tsLang.javascriptDefaults.setDiagnosticsOptions(diagnosticsOptions);
|
|
465
|
+
|
|
466
|
+
// Allow importing from node_modules type definitions
|
|
467
|
+
tsLang.typescriptDefaults.setEagerModelSync(true);
|
|
468
|
+
tsLang.javascriptDefaults.setEagerModelSync(true);
|
|
469
|
+
|
|
470
|
+
// Keep these custom libs stable: ATA may load overlapping package paths later.
|
|
471
|
+
if (monacoSandboxTypeLibsInjected) return;
|
|
472
|
+
|
|
473
|
+
// Stub Node.js globals (process, Buffer, etc.) so sandbox code using them doesn't error
|
|
474
|
+
const nodeGlobals = `
|
|
475
|
+
declare var process: {
|
|
476
|
+
env: Record<string, string | undefined> & {
|
|
477
|
+
NODE_ENV: 'development' | 'production' | 'test';
|
|
478
|
+
REMOTE_PORT?: string;
|
|
479
|
+
PORT?: string;
|
|
480
|
+
HOST?: string;
|
|
481
|
+
[key: string]: string | undefined;
|
|
482
|
+
};
|
|
483
|
+
argv: string[];
|
|
484
|
+
cwd(): string;
|
|
485
|
+
exit(code?: number): never;
|
|
486
|
+
platform: string;
|
|
487
|
+
version: string;
|
|
488
|
+
versions: Record<string, string>;
|
|
489
|
+
};
|
|
490
|
+
declare var Buffer: {
|
|
491
|
+
from(data: string | ArrayBuffer | ArrayBufferView, encoding?: string): { toString(encoding?: string): string };
|
|
492
|
+
alloc(size: number): Uint8Array;
|
|
493
|
+
isBuffer(obj: unknown): boolean;
|
|
494
|
+
};
|
|
495
|
+
declare var __dirname: string;
|
|
496
|
+
declare var __filename: string;
|
|
497
|
+
`;
|
|
498
|
+
tsLang.typescriptDefaults.addExtraLib(
|
|
499
|
+
nodeGlobals,
|
|
500
|
+
"file:///__sandbox-types/node-globals.d.ts",
|
|
501
|
+
);
|
|
502
|
+
tsLang.javascriptDefaults.addExtraLib(
|
|
503
|
+
nodeGlobals,
|
|
504
|
+
"file:///__sandbox-types/node-globals.d.ts",
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// Pre-seed React + JSX ambient types so TSX has intrinsic element typing.
|
|
508
|
+
const reactTypes = `
|
|
509
|
+
declare module 'react' {
|
|
510
|
+
export type ReactNode = any;
|
|
511
|
+
export type ReactElement = any;
|
|
512
|
+
export type CSSProperties = Record<string, string | number>;
|
|
513
|
+
export interface FC<P = {}> {
|
|
514
|
+
(props: P): ReactElement | null;
|
|
515
|
+
}
|
|
516
|
+
export interface AnchorHTMLAttributes<T> extends Record<string, unknown> {}
|
|
517
|
+
export function useEffect(
|
|
518
|
+
effect: () => void | (() => void),
|
|
519
|
+
deps?: readonly unknown[],
|
|
520
|
+
): void;
|
|
521
|
+
export function useState<S>(
|
|
522
|
+
initialState: S | (() => S),
|
|
523
|
+
): [S, (value: S | ((prev: S) => S)) => void];
|
|
524
|
+
export function useMemo<T>(factory: () => T, deps: readonly unknown[]): T;
|
|
525
|
+
export function useCallback<T extends (...args: any[]) => any>(
|
|
526
|
+
callback: T,
|
|
527
|
+
deps: readonly unknown[],
|
|
528
|
+
): T;
|
|
529
|
+
export function useRef<T>(initialValue: T): { current: T };
|
|
530
|
+
const React: {
|
|
531
|
+
createElement: (...args: any[]) => ReactElement;
|
|
532
|
+
Fragment: any;
|
|
533
|
+
};
|
|
534
|
+
export default React;
|
|
535
|
+
}
|
|
536
|
+
declare module 'react/jsx-runtime' {
|
|
537
|
+
export const Fragment: any;
|
|
538
|
+
export function jsx(type: any, props: any, key?: any): any;
|
|
539
|
+
export function jsxs(type: any, props: any, key?: any): any;
|
|
540
|
+
export function jsxDEV(
|
|
541
|
+
type: any,
|
|
542
|
+
props: any,
|
|
543
|
+
key: any,
|
|
544
|
+
isStaticChildren: boolean,
|
|
545
|
+
source: any,
|
|
546
|
+
self: any,
|
|
547
|
+
): any;
|
|
548
|
+
}
|
|
549
|
+
declare namespace JSX {
|
|
550
|
+
interface Element {}
|
|
551
|
+
interface IntrinsicElements {
|
|
552
|
+
[elemName: string]: any;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
`;
|
|
556
|
+
tsLang.typescriptDefaults.addExtraLib(
|
|
557
|
+
reactTypes,
|
|
558
|
+
"file:///__sandbox-types/react-jsx.d.ts",
|
|
559
|
+
);
|
|
560
|
+
tsLang.javascriptDefaults.addExtraLib(
|
|
561
|
+
reactTypes,
|
|
562
|
+
"file:///__sandbox-types/react-jsx.d.ts",
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// Pre-seed Next.js type stubs. ATA cannot resolve `next` because Next uses a
|
|
566
|
+
// complex package.json exports map that unpkg / CDN ATA doesn't traverse.
|
|
567
|
+
const nextTypes = `
|
|
568
|
+
declare module 'next' {
|
|
569
|
+
export type NextConfig = {
|
|
570
|
+
reactStrictMode?: boolean;
|
|
571
|
+
swcMinify?: boolean;
|
|
572
|
+
output?: 'standalone' | 'export';
|
|
573
|
+
compress?: boolean;
|
|
574
|
+
poweredByHeader?: boolean;
|
|
575
|
+
distDir?: string;
|
|
576
|
+
assetPrefix?: string;
|
|
577
|
+
basePath?: string;
|
|
578
|
+
trailingSlash?: boolean;
|
|
579
|
+
pageExtensions?: string[];
|
|
580
|
+
env?: Record<string, string>;
|
|
581
|
+
images?: {
|
|
582
|
+
domains?: string[];
|
|
583
|
+
remotePatterns?: { protocol?: 'http' | 'https'; hostname: string; port?: string; pathname?: string }[];
|
|
584
|
+
formats?: ('image/avif' | 'image/webp')[];
|
|
585
|
+
unoptimized?: boolean;
|
|
586
|
+
};
|
|
587
|
+
experimental?: { serverActions?: boolean | { allowedOrigins?: string[] }; [key: string]: unknown };
|
|
588
|
+
webpack?: (config: any, options: { buildId: string; dev: boolean; isServer: boolean; defaultLoaders: any; webpack: any }) => any;
|
|
589
|
+
rewrites?: () => Promise<{ source: string; destination: string }[] | { beforeFiles?: any[]; afterFiles?: any[]; fallback?: any[] }>;
|
|
590
|
+
redirects?: () => Promise<{ source: string; destination: string; permanent: boolean }[]>;
|
|
591
|
+
headers?: () => Promise<{ source: string; headers: { key: string; value: string }[] }[]>;
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
declare module 'next/navigation' {
|
|
595
|
+
export function useRouter(): { push(href: string): void; replace(href: string): void; back(): void; forward(): void; refresh(): void; prefetch(href: string): void };
|
|
596
|
+
export function usePathname(): string;
|
|
597
|
+
export function useSearchParams(): URLSearchParams;
|
|
598
|
+
export function useParams<T extends Record<string, string | string[]> = Record<string, string | string[]>>(): T;
|
|
599
|
+
export function redirect(url: string): never;
|
|
600
|
+
export function notFound(): never;
|
|
601
|
+
}
|
|
602
|
+
declare module 'next/link' {
|
|
603
|
+
import * as React from 'react';
|
|
604
|
+
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> { href: string | { pathname?: string; query?: Record<string, string> }; replace?: boolean; prefetch?: boolean; }
|
|
605
|
+
const Link: React.FC<LinkProps>;
|
|
606
|
+
export default Link;
|
|
607
|
+
}
|
|
608
|
+
declare module 'next/image' {
|
|
609
|
+
import * as React from 'react';
|
|
610
|
+
export interface ImageProps { src: string; alt: string; width?: number; height?: number; fill?: boolean; priority?: boolean; quality?: number; className?: string; style?: React.CSSProperties; }
|
|
611
|
+
const Image: React.FC<ImageProps>;
|
|
612
|
+
export default Image;
|
|
613
|
+
}
|
|
614
|
+
declare module 'next/font/google' {
|
|
615
|
+
export interface FontOptions { subsets?: string[]; weight?: string | string[]; style?: string | string[]; variable?: string; display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional'; }
|
|
616
|
+
export type FontResult = { className: string; style: { fontFamily: string; fontWeight?: number; fontStyle?: string }; variable: string };
|
|
617
|
+
export function Inter(options?: FontOptions): FontResult;
|
|
618
|
+
export function Roboto(options?: FontOptions): FontResult;
|
|
619
|
+
export function Geist(options?: FontOptions): FontResult;
|
|
620
|
+
export function Geist_Mono(options?: FontOptions): FontResult;
|
|
621
|
+
}
|
|
622
|
+
declare module 'next/server' {
|
|
623
|
+
export class NextResponse extends Response {
|
|
624
|
+
static json<T>(data: T, init?: ResponseInit): NextResponse;
|
|
625
|
+
static redirect(url: string | URL, status?: number): NextResponse;
|
|
626
|
+
static next(): NextResponse;
|
|
627
|
+
}
|
|
628
|
+
export class NextRequest extends Request {
|
|
629
|
+
readonly nextUrl: URL;
|
|
630
|
+
readonly cookies: { get(name: string): { name: string; value: string } | undefined; getAll(): { name: string; value: string }[] };
|
|
631
|
+
}
|
|
632
|
+
export type NextMiddleware = (request: NextRequest) => Response | NextResponse | void | Promise<Response | NextResponse | void>;
|
|
633
|
+
}
|
|
634
|
+
`;
|
|
635
|
+
tsLang.typescriptDefaults.addExtraLib(
|
|
636
|
+
nextTypes,
|
|
637
|
+
"file:///__sandbox-types/next.d.ts",
|
|
638
|
+
);
|
|
639
|
+
tsLang.javascriptDefaults.addExtraLib(
|
|
640
|
+
nextTypes,
|
|
641
|
+
"file:///__sandbox-types/next.d.ts",
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
monacoSandboxTypeLibsInjected = true;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const handleMount: OnMount = (editor, monaco) => {
|
|
648
|
+
monacoRef.current = monaco;
|
|
649
|
+
initATA();
|
|
650
|
+
// Kick off an initial scan for any imports already in the file
|
|
651
|
+
acquireTypes(value);
|
|
652
|
+
if (autoFocus) editor.focus();
|
|
653
|
+
if (onCtrlEnter) {
|
|
654
|
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () =>
|
|
655
|
+
onCtrlEnter(),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// Re-scan for new imports whenever code changes
|
|
661
|
+
useEffect(() => {
|
|
662
|
+
acquireTypes(value);
|
|
663
|
+
}, [value, acquireTypes]);
|
|
664
|
+
|
|
665
|
+
const modelPath =
|
|
666
|
+
path && path.length
|
|
667
|
+
? path.startsWith("file://")
|
|
668
|
+
? path
|
|
669
|
+
: `file:///${path.replace(/^\/+/, "")}`
|
|
670
|
+
: undefined;
|
|
671
|
+
|
|
672
|
+
return (
|
|
673
|
+
<div className="absolute inset-0">
|
|
674
|
+
<MonacoEditorLib
|
|
675
|
+
key={modelPath ?? language}
|
|
676
|
+
height="100%"
|
|
677
|
+
width="100%"
|
|
678
|
+
language={language.includes("typescript") ? "typescript" : "javascript"}
|
|
679
|
+
path={modelPath}
|
|
680
|
+
theme="vs-dark"
|
|
681
|
+
value={value}
|
|
682
|
+
onChange={(val) => onChange(val ?? "")}
|
|
683
|
+
beforeMount={handleBeforeMount}
|
|
684
|
+
onMount={handleMount}
|
|
685
|
+
options={{
|
|
686
|
+
fontSize: fontSizePx,
|
|
687
|
+
fontFamily: EDITOR_FONT,
|
|
688
|
+
minimap: { enabled: false },
|
|
689
|
+
scrollBeyondLastLine: false,
|
|
690
|
+
lineNumbers: "on",
|
|
691
|
+
glyphMargin: false,
|
|
692
|
+
folding: false,
|
|
693
|
+
lineDecorationsWidth: 4,
|
|
694
|
+
lineNumbersMinChars: 3,
|
|
695
|
+
renderLineHighlight: "line",
|
|
696
|
+
overviewRulerLanes: 0,
|
|
697
|
+
hideCursorInOverviewRuler: true,
|
|
698
|
+
overviewRulerBorder: false,
|
|
699
|
+
scrollbar: { vertical: "auto", horizontal: "auto" },
|
|
700
|
+
padding: { top: 8, bottom: 8 },
|
|
701
|
+
wordWrap: "off",
|
|
702
|
+
automaticLayout: true,
|
|
703
|
+
placeholder,
|
|
704
|
+
}}
|
|
705
|
+
/>
|
|
706
|
+
</div>
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
364
710
|
interface FileTreeNode {
|
|
365
711
|
name: string;
|
|
366
712
|
path: string;
|
|
@@ -674,6 +1020,9 @@ export default function CodeRunnerModal() {
|
|
|
674
1020
|
runnerInitialFileId ?? null,
|
|
675
1021
|
);
|
|
676
1022
|
|
|
1023
|
+
// ── Editor mode ───────────────────────────────────────────
|
|
1024
|
+
const [useMonacoEditor, setUseMonacoEditor] = useState(true);
|
|
1025
|
+
|
|
677
1026
|
// ── Sandbox state ─────────────────────────────────────────
|
|
678
1027
|
const [mode, setMode] = useState<"script" | "sandbox">("script");
|
|
679
1028
|
const [serverCode, setServerCode] = useState(DEFAULT_SERVER_CODE);
|
|
@@ -698,6 +1047,9 @@ export default function CodeRunnerModal() {
|
|
|
698
1047
|
const [reactPreviewSrc, setReactPreviewSrc] = useState<string | null>(null);
|
|
699
1048
|
const [reactAddingFile, setReactAddingFile] = useState(false);
|
|
700
1049
|
const [reactNewFileName, setReactNewFileName] = useState("");
|
|
1050
|
+
// Explorer sidebar width (resizable via drag handle)
|
|
1051
|
+
const [explorerWidth, setExplorerWidth] = useState(144);
|
|
1052
|
+
const explorerDragStart = useRef<{ x: number; w: number } | null>(null);
|
|
701
1053
|
// Folders that are collapsed in the Next.js file tree sidebar
|
|
702
1054
|
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(
|
|
703
1055
|
new Set(),
|
|
@@ -3464,6 +3816,26 @@ export default function CodeRunnerModal() {
|
|
|
3464
3816
|
</>
|
|
3465
3817
|
)}
|
|
3466
3818
|
|
|
3819
|
+
{/* Monaco toggle */}
|
|
3820
|
+
<button
|
|
3821
|
+
type="button"
|
|
3822
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
3823
|
+
onClick={() => setUseMonacoEditor((v) => !v)}
|
|
3824
|
+
className={`flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-mono font-medium transition-colors shrink-0 ${
|
|
3825
|
+
useMonacoEditor
|
|
3826
|
+
? "bg-violet-600/30 text-violet-300 ring-1 ring-violet-500/40"
|
|
3827
|
+
: "text-slate-500 hover:text-slate-300 hover:bg-slate-700"
|
|
3828
|
+
}`}
|
|
3829
|
+
title={
|
|
3830
|
+
useMonacoEditor
|
|
3831
|
+
? "Switch to simple editor"
|
|
3832
|
+
: "Switch to Monaco editor"
|
|
3833
|
+
}
|
|
3834
|
+
>
|
|
3835
|
+
<Code2 className="w-3 h-3" />
|
|
3836
|
+
Monaco
|
|
3837
|
+
</button>
|
|
3838
|
+
|
|
3467
3839
|
<button
|
|
3468
3840
|
onMouseDown={(e) => e.stopPropagation()}
|
|
3469
3841
|
onClick={toggleMax}
|
|
@@ -3491,16 +3863,31 @@ export default function CodeRunnerModal() {
|
|
|
3491
3863
|
<div className="flex flex-col flex-1 overflow-hidden">
|
|
3492
3864
|
{/* Editor pane */}
|
|
3493
3865
|
<div className="flex-1 min-h-0 relative">
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3866
|
+
{useMonacoEditor ? (
|
|
3867
|
+
<MonacoEditorWrapper
|
|
3868
|
+
value={code}
|
|
3869
|
+
onChange={setCode}
|
|
3870
|
+
onCtrlEnter={() => void runCode()}
|
|
3871
|
+
language={lang}
|
|
3872
|
+
path={
|
|
3873
|
+
lang === "typescript" ? "sandbox/main.ts" : "sandbox/main.js"
|
|
3874
|
+
}
|
|
3875
|
+
autoFocus
|
|
3876
|
+
fontSize="13px"
|
|
3877
|
+
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);`}
|
|
3878
|
+
/>
|
|
3879
|
+
) : (
|
|
3880
|
+
<SyntaxEditor
|
|
3881
|
+
value={code}
|
|
3882
|
+
onChange={setCode}
|
|
3883
|
+
onCtrlEnter={() => void runCode()}
|
|
3884
|
+
language={lang}
|
|
3885
|
+
autoFocus
|
|
3886
|
+
fontSize="13px"
|
|
3887
|
+
focusRingClass="ring-violet-500/40"
|
|
3888
|
+
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);`}
|
|
3889
|
+
/>
|
|
3890
|
+
)}
|
|
3504
3891
|
</div>
|
|
3505
3892
|
|
|
3506
3893
|
{/* Divider */}
|
|
@@ -3655,17 +4042,35 @@ export default function CodeRunnerModal() {
|
|
|
3655
4042
|
)}
|
|
3656
4043
|
</div>
|
|
3657
4044
|
<div className="flex-1 min-h-0 relative">
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
4045
|
+
{useMonacoEditor ? (
|
|
4046
|
+
<MonacoEditorWrapper
|
|
4047
|
+
value={serverCode}
|
|
4048
|
+
onChange={setServerCode}
|
|
4049
|
+
onCtrlEnter={() => void startServer()}
|
|
4050
|
+
language={serverLang}
|
|
4051
|
+
path={
|
|
4052
|
+
serverLang === "typescript"
|
|
4053
|
+
? "sandbox/server.ts"
|
|
4054
|
+
: "sandbox/server.js"
|
|
4055
|
+
}
|
|
4056
|
+
fontSize="12px"
|
|
4057
|
+
placeholder={
|
|
4058
|
+
"import express from 'express';\n// process.env.PORT is auto-injected\n// Ctrl+Enter to start"
|
|
4059
|
+
}
|
|
4060
|
+
/>
|
|
4061
|
+
) : (
|
|
4062
|
+
<SyntaxEditor
|
|
4063
|
+
value={serverCode}
|
|
4064
|
+
onChange={setServerCode}
|
|
4065
|
+
onCtrlEnter={() => void startServer()}
|
|
4066
|
+
language={serverLang}
|
|
4067
|
+
fontSize="12px"
|
|
4068
|
+
focusRingClass="ring-emerald-500/30"
|
|
4069
|
+
placeholder={
|
|
4070
|
+
"import express from 'express';\n// process.env.PORT is auto-injected\n// Ctrl+Enter to start"
|
|
4071
|
+
}
|
|
4072
|
+
/>
|
|
4073
|
+
)}
|
|
3669
4074
|
</div>
|
|
3670
4075
|
</div>
|
|
3671
4076
|
)}
|
|
@@ -4057,7 +4462,10 @@ export default function CodeRunnerModal() {
|
|
|
4057
4462
|
>
|
|
4058
4463
|
{/* ── VS Code-style file tree sidebar ── */}
|
|
4059
4464
|
{usesClientExplorer && (
|
|
4060
|
-
<div
|
|
4465
|
+
<div
|
|
4466
|
+
className="shrink-0 flex flex-col border-r border-slate-700 bg-slate-900/60 overflow-y-auto relative"
|
|
4467
|
+
style={{ width: explorerWidth }}
|
|
4468
|
+
>
|
|
4061
4469
|
{/* Sidebar header */}
|
|
4062
4470
|
<div className="flex items-center justify-between px-2 py-1.5 border-b border-slate-700/60">
|
|
4063
4471
|
<span className="text-[9px] uppercase tracking-widest text-slate-500 font-semibold select-none">
|
|
@@ -4301,6 +4709,37 @@ export default function CodeRunnerModal() {
|
|
|
4301
4709
|
return renderNode(tree);
|
|
4302
4710
|
})()}
|
|
4303
4711
|
</div>
|
|
4712
|
+
{/* Drag handle — inside the relative explorer container */}
|
|
4713
|
+
<div
|
|
4714
|
+
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"
|
|
4715
|
+
onMouseDown={(e) => {
|
|
4716
|
+
e.preventDefault();
|
|
4717
|
+
explorerDragStart.current = {
|
|
4718
|
+
x: e.clientX,
|
|
4719
|
+
w: explorerWidth,
|
|
4720
|
+
};
|
|
4721
|
+
const onMove = (ev: MouseEvent) => {
|
|
4722
|
+
if (!explorerDragStart.current) return;
|
|
4723
|
+
const newW = Math.max(
|
|
4724
|
+
100,
|
|
4725
|
+
Math.min(
|
|
4726
|
+
320,
|
|
4727
|
+
explorerDragStart.current.w +
|
|
4728
|
+
ev.clientX -
|
|
4729
|
+
explorerDragStart.current.x,
|
|
4730
|
+
),
|
|
4731
|
+
);
|
|
4732
|
+
setExplorerWidth(newW);
|
|
4733
|
+
};
|
|
4734
|
+
const onUp = () => {
|
|
4735
|
+
explorerDragStart.current = null;
|
|
4736
|
+
window.removeEventListener("mousemove", onMove);
|
|
4737
|
+
window.removeEventListener("mouseup", onUp);
|
|
4738
|
+
};
|
|
4739
|
+
window.addEventListener("mousemove", onMove);
|
|
4740
|
+
window.addEventListener("mouseup", onUp);
|
|
4741
|
+
}}
|
|
4742
|
+
/>
|
|
4304
4743
|
</div>
|
|
4305
4744
|
)}
|
|
4306
4745
|
|
|
@@ -4363,6 +4802,24 @@ export default function CodeRunnerModal() {
|
|
|
4363
4802
|
</div>
|
|
4364
4803
|
)}
|
|
4365
4804
|
</div>
|
|
4805
|
+
) : useMonacoEditor ? (
|
|
4806
|
+
<MonacoEditorWrapper
|
|
4807
|
+
value={clientCode}
|
|
4808
|
+
onChange={setClientCode}
|
|
4809
|
+
onCtrlEnter={() => {
|
|
4810
|
+
if (serverRunning) void runClient();
|
|
4811
|
+
}}
|
|
4812
|
+
language={clientLang}
|
|
4813
|
+
path={
|
|
4814
|
+
clientLang === "typescript"
|
|
4815
|
+
? "sandbox/client.ts"
|
|
4816
|
+
: "sandbox/client.js"
|
|
4817
|
+
}
|
|
4818
|
+
fontSize="12px"
|
|
4819
|
+
placeholder={
|
|
4820
|
+
"// SANDBOX_URL is injected automatically\n// Start the server first, then Ctrl+Enter to run"
|
|
4821
|
+
}
|
|
4822
|
+
/>
|
|
4366
4823
|
) : (
|
|
4367
4824
|
<SyntaxEditor
|
|
4368
4825
|
value={clientCode}
|
|
@@ -4400,8 +4857,8 @@ export default function CodeRunnerModal() {
|
|
|
4400
4857
|
)}
|
|
4401
4858
|
</div>
|
|
4402
4859
|
</div>
|
|
4403
|
-
) : (
|
|
4404
|
-
<
|
|
4860
|
+
) : useMonacoEditor ? (
|
|
4861
|
+
<MonacoEditorWrapper
|
|
4405
4862
|
key={reactActiveFile}
|
|
4406
4863
|
value={reactFiles[reactActiveFile] ?? ""}
|
|
4407
4864
|
onChange={(val) =>
|
|
@@ -4416,6 +4873,29 @@ export default function CodeRunnerModal() {
|
|
|
4416
4873
|
? "typescript"
|
|
4417
4874
|
: "javascript"
|
|
4418
4875
|
}
|
|
4876
|
+
path={reactActiveFile}
|
|
4877
|
+
fontSize="12px"
|
|
4878
|
+
placeholder={`// ${reactActiveFile}\n`}
|
|
4879
|
+
/>
|
|
4880
|
+
) : (
|
|
4881
|
+
<SyntaxEditor
|
|
4882
|
+
key={reactActiveFile}
|
|
4883
|
+
value={reactFiles[reactActiveFile] ?? ""}
|
|
4884
|
+
onChange={(val) =>
|
|
4885
|
+
setReactFiles((prev) => ({
|
|
4886
|
+
...prev,
|
|
4887
|
+
[reactActiveFile]: val,
|
|
4888
|
+
}))
|
|
4889
|
+
}
|
|
4890
|
+
language={
|
|
4891
|
+
reactActiveFile.endsWith(".tsx")
|
|
4892
|
+
? "typescriptreact"
|
|
4893
|
+
: reactActiveFile.endsWith(".ts")
|
|
4894
|
+
? "typescript"
|
|
4895
|
+
: reactActiveFile.endsWith(".jsx")
|
|
4896
|
+
? "javascriptreact"
|
|
4897
|
+
: "javascript"
|
|
4898
|
+
}
|
|
4419
4899
|
fontSize="12px"
|
|
4420
4900
|
focusRingClass="ring-cyan-500/30"
|
|
4421
4901
|
placeholder={`// ${reactActiveFile}\n`}
|