create-interview-cockpit 0.15.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.
@@ -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
- <SyntaxEditor
3495
- value={code}
3496
- onChange={setCode}
3497
- onCtrlEnter={() => void runCode()}
3498
- language={lang}
3499
- autoFocus
3500
- fontSize="13px"
3501
- focusRingClass="ring-violet-500/40"
3502
- 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);`}
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
- <SyntaxEditor
3659
- value={serverCode}
3660
- onChange={setServerCode}
3661
- onCtrlEnter={() => void startServer()}
3662
- language={serverLang}
3663
- fontSize="12px"
3664
- focusRingClass="ring-emerald-500/30"
3665
- placeholder={
3666
- "import express from 'express';\n// process.env.PORT is auto-injected\n// Ctrl+Enter to start"
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 className="w-36 shrink-0 flex flex-col border-r border-slate-700 bg-slate-900/60 overflow-y-auto">
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}
@@ -4,6 +4,11 @@ import { parseInfraLabWorkspace } from "../infraLab";
4
4
  import {
5
5
  parseFrontendLabWorkspace,
6
6
  ISOLATED_MODULE_FEDERATION_LAB,
7
+ NEXTJS_MF_PLUGIN_LAB,
8
+ NEXTJS_MF_RUNTIME_LAB,
9
+ NEXTJS_MULTI_ZONES_LAB,
10
+ NEXTJS_MF_RUNTIME_API_LAB,
11
+ RSPACK_SHELL_LAB,
7
12
  } from "../reactLab";
8
13
  import { BROWSER_SECURITY_TEMPLATES } from "../browserSecurityTemplates";
9
14
  import type { ContextFile } from "../types";
@@ -23,6 +28,7 @@ import {
23
28
  Link2Off,
24
29
  Network,
25
30
  Shield,
31
+ PenLine,
26
32
  } from "lucide-react";
27
33
 
28
34
  // ─── Helpers ─────────────────────────────────────────────
@@ -34,6 +40,7 @@ const LAB_ORIGINS = new Set([
34
40
  "react",
35
41
  "nextjs",
36
42
  "module-federation",
43
+ "canvas",
37
44
  ]);
38
45
 
39
46
  function isLabFile(cf: ContextFile) {
@@ -193,6 +200,7 @@ export default function LabsPanel() {
193
200
  openNextLab,
194
201
  openModuleFederationLab,
195
202
  openDeploymentLab,
203
+ openCanvasLab,
196
204
  removeQuestionFile,
197
205
  detachLabFile,
198
206
  attachLabFile,
@@ -406,6 +414,17 @@ export default function LabsPanel() {
406
414
  }
407
415
  };
408
416
 
417
+ const openCanvasFile = async (cf: ContextFile) => {
418
+ try {
419
+ const raw = await fetch(`/api/context-files/${cf.id}/content`)
420
+ .then((r) => r.json())
421
+ .then((d) => d.content as string);
422
+ openCanvasLab(raw, cf.id);
423
+ } catch {
424
+ /* ignore */
425
+ }
426
+ };
427
+
409
428
  // ── Section renderer ─────────────────────────────────────
410
429
 
411
430
  function Section({
@@ -640,12 +659,56 @@ export default function LabsPanel() {
640
659
  onClick: () =>
641
660
  openModuleFederationLab(ISOLATED_MODULE_FEDERATION_LAB),
642
661
  },
662
+ {
663
+ label: "Next.js MF — Plugin (Option A)",
664
+ description:
665
+ "Next.js shell + remote via built-in webpack.container.ModuleFederationPlugin",
666
+ onClick: () => openModuleFederationLab(NEXTJS_MF_PLUGIN_LAB),
667
+ },
668
+ {
669
+ label: "Next.js MF — Runtime Loader (Option B)",
670
+ description:
671
+ "Next.js shell with no plugin — loads remote via plain script injection",
672
+ onClick: () => openModuleFederationLab(NEXTJS_MF_RUNTIME_LAB),
673
+ },
674
+ {
675
+ label: "Next.js — Multi-Zones",
676
+ description:
677
+ "Two independent Next.js apps split by URL path via rewrites — Vercel recommended",
678
+ onClick: () => openModuleFederationLab(NEXTJS_MULTI_ZONES_LAB),
679
+ },
680
+ {
681
+ label: "Next.js — MF Runtime API",
682
+ description:
683
+ "@module-federation/enhanced/runtime inside a 'use client' component — no webpack config",
684
+ onClick: () =>
685
+ openModuleFederationLab(NEXTJS_MF_RUNTIME_API_LAB),
686
+ },
687
+ {
688
+ label: "Rspack Shell — Native MF 2.0",
689
+ description:
690
+ "Rspack as the host with built-in MF support; webpack app as the remote",
691
+ onClick: () => openModuleFederationLab(RSPACK_SHELL_LAB),
692
+ },
643
693
  ]}
644
694
  onOpen={openMFFile}
645
695
  openTitle="Open in Webpack Module Federation Lab"
646
696
  accentClass="text-emerald-200"
647
697
  bgClass="bg-emerald-500/10 border border-emerald-500/20"
648
698
  />
699
+ <Section
700
+ title="Canvas Labs"
701
+ icon={PenLine}
702
+ iconColor="text-orange-400/70"
703
+ origin="canvas"
704
+ emptyText="Save a canvas lab to reopen it here"
705
+ onNewLab={() => openCanvasLab()}
706
+ newLabTitle="Open Canvas Lab"
707
+ onOpen={openCanvasFile}
708
+ openTitle="Open in Canvas Lab"
709
+ accentClass="text-orange-200"
710
+ bgClass="bg-orange-500/10 border border-orange-500/20"
711
+ />
649
712
  </div>
650
713
  ) : (
651
714
  <div className="flex-1 flex items-center justify-center">
@@ -154,6 +154,9 @@ export default function Sidebar() {
154
154
 
155
155
  // Drive subfolder navigator
156
156
  const activeWs = workspaces.find((w) => w.id === activeWorkspaceId);
157
+ const activeWsSortOrder = (activeWs?.questionSortOrder ?? "name") as
158
+ | "name"
159
+ | "createdAt";
157
160
  const isDriveWs =
158
161
  activeWs?.type === "google_drive" && !!activeWs.driveConfig?.folderId;
159
162
  const currentSubFolder = activeWs?.driveConfig?.subFolderId
@@ -804,7 +807,15 @@ export default function Sidebar() {
804
807
  )
805
808
  .map((topic) => {
806
809
  const isExpanded = expandedTopics.includes(topic.id);
807
- const questions = questionsByTopic[topic.id] || [];
810
+ const questions = [...(questionsByTopic[topic.id] || [])].sort(
811
+ (a, b) =>
812
+ activeWsSortOrder === "createdAt"
813
+ ? a.createdAt.localeCompare(b.createdAt)
814
+ : a.title.localeCompare(b.title, undefined, {
815
+ numeric: true,
816
+ sensitivity: "base",
817
+ }),
818
+ );
808
819
 
809
820
  return (
810
821
  <div key={topic.id}>
@@ -27,6 +27,7 @@ export default function WorkspaceSwitcher() {
27
27
  createWorkspace,
28
28
  deleteWorkspace,
29
29
  renameWorkspace,
30
+ patchWorkspace,
30
31
  syncWorkspace,
31
32
  linkDriveFolder,
32
33
  attachDriveFolder,
@@ -814,6 +815,41 @@ export default function WorkspaceSwitcher() {
814
815
  )}
815
816
  </div>
816
817
  )}
818
+
819
+ {/* Question sort order */}
820
+ <div
821
+ className="mt-1 ml-5 flex items-center gap-1"
822
+ onClick={(e) => e.stopPropagation()}
823
+ >
824
+ <span className="text-[10px] text-slate-600">Order:</span>
825
+ <button
826
+ onClick={() =>
827
+ patchWorkspace(ws.id, { questionSortOrder: "name" })
828
+ }
829
+ className={`text-[10px] px-1 rounded transition-colors ${
830
+ (ws.questionSortOrder ?? "name") === "name"
831
+ ? "text-cyan-400"
832
+ : "text-slate-600 hover:text-slate-400"
833
+ }`}
834
+ >
835
+ Name
836
+ </button>
837
+ <span className="text-[10px] text-slate-700">·</span>
838
+ <button
839
+ onClick={() =>
840
+ patchWorkspace(ws.id, {
841
+ questionSortOrder: "createdAt",
842
+ })
843
+ }
844
+ className={`text-[10px] px-1 rounded transition-colors ${
845
+ ws.questionSortOrder === "createdAt"
846
+ ? "text-cyan-400"
847
+ : "text-slate-600 hover:text-slate-400"
848
+ }`}
849
+ >
850
+ Date created
851
+ </button>
852
+ </div>
817
853
  </div>
818
854
  ))}
819
855
  </div>