feat(client): using monaco editor in interactive editor (#64601)

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
Gary Yeung
2026-01-21 15:51:37 +08:00
committed by GitHub
parent e65064a8ea
commit f867807edd
4 changed files with 184 additions and 11 deletions
@@ -0,0 +1,9 @@
.monaco-editor-wrapper {
padding-block-start: 8px;
flex: 1;
}
.monaco-editor .line-numbers {
color: #858591 !important;
font-size: 12px;
}
@@ -0,0 +1,101 @@
import React from 'react';
import Loadable from '@loadable/component';
import type { EditorWillMount, monaco } from 'react-monaco-editor';
import { useActiveCode, useSandpack } from '@codesandbox/sandpack-react';
import './custom-monaco-editor.css';
const MonacoEditor = Loadable(() => import('react-monaco-editor'));
const CustomMonacoEditor = () => {
const { code, updateCode } = useActiveCode();
const { sandpack } = useSandpack();
const getLanguage = (filePath: string): string => {
const extension = filePath.split('.').pop();
switch (extension) {
case 'js':
case 'jsx':
return 'javascript';
case 'ts':
case 'tsx':
return 'typescript';
case 'html':
return 'html';
case 'css':
return 'css';
default:
return 'plaintext';
}
};
// theme
const FCC_DARK_CUSTOM: monaco.editor.IStandaloneThemeData = {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'comment', foreground: '858591', fontStyle: 'italic' },
{ token: 'keyword', foreground: 'dbb8ff' },
{ token: 'tag', foreground: 'f07178' },
{ token: 'punctuation', foreground: '99c9ff' },
{ token: 'definition', foreground: 'ffffff' },
{ token: 'property', foreground: '99c9ff' },
{ token: 'static', foreground: 'f78c6c' },
{ token: 'string', foreground: 'acd157' },
{ token: 'number', foreground: 'f78c6c' },
{ token: 'variable', foreground: 'ffffff' },
{ token: 'type', foreground: 'dbb8ff' },
{ token: 'function', foreground: 'ffffff' },
{ token: 'identifier', foreground: 'ffffff' },
{ token: 'regexp', foreground: 'acd157' },
{ token: 'delimiter', foreground: 'ffffff' },
{ token: 'attribute.name', foreground: '99c9ff' },
{ token: 'attribute.value', foreground: 'acd157' },
{ token: 'annotation', foreground: 'dbb8ff' },
{ token: 'constant', foreground: 'f78c6c' },
{ token: 'class', foreground: 'dbb8ff' },
{ token: 'interface', foreground: 'dbb9ff' },
{ token: 'namespace', foreground: 'dbb8ff' },
{ token: 'enum', foreground: 'dbb8ff' },
{ token: 'operator', foreground: 'ffffff' }
],
colors: {
'editor.background': '#0a0a23', // surface1
'editor.foreground': '#ffffff', // base (plain syntax)
'editorCursor.foreground': '#ffffff',
'editor.selectionBackground': '#3b3b4f',
'editor.lineHighlightBackground': '#3b3b4f', // surface3
'editorBracketMatch.border': '#dbb8ff'
}
};
const handleEditor: EditorWillMount = monaco => {
monaco.editor.defineTheme('fcc-dark', FCC_DARK_CUSTOM);
};
return (
<div className='monaco-editor-wrapper'>
<MonacoEditor
width='100%'
height='100%'
language={getLanguage(sandpack.activeFile)}
theme='fcc-dark'
editorWillMount={handleEditor}
key={sandpack.activeFile}
defaultValue={code}
onChange={value => {
updateCode(value || '');
}}
options={{
lineNumbersMinChars: 2,
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true
}}
/>
</div>
);
};
CustomMonacoEditor.displayname = 'CustomMonacoEditor';
export default CustomMonacoEditor;
@@ -27,3 +27,39 @@
color: var(--gray-90) !important;
border-color: var(--gray-90) !important;
}
.interactive-layout {
display: flex;
flex-direction: row; /* Default for desktop: side-by-side */
height: 450px;
}
.interactive-editor-column {
flex: 1.5 !important;
height: 100% !important;
}
.interactive-preview-column {
flex: 1 !important;
height: 100% !important;
}
.sp-preview {
height: 100%;
}
@media (max-width: 768px) {
.interactive-layout {
flex-direction: column;
height: auto !important; /* Allow the height to expand as content stacks */
}
.interactive-editor-column,
.interactive-preview-column {
width: 100%;
/* Override desktop flex value */
flex: 0 0 auto !important;
/* Set a reasonable mobile height for the editor */
height: 400px !important;
}
}
@@ -1,7 +1,15 @@
import React, { useMemo } from 'react';
import { Sandpack } from '@codesandbox/sandpack-react';
import {
FileTabs,
SandpackConsole,
SandpackLayout,
SandpackPreview,
SandpackProvider,
SandpackStack
} from '@codesandbox/sandpack-react';
import { freeCodeCampDark } from '@codesandbox/sandpack-themes';
import './interactive-editor.css';
import CustomMonacoEditor from './custom-monaco-editor';
export interface InteractiveFile {
ext: string;
@@ -61,7 +69,7 @@ const InteractiveEditor = ({ files }: Props) => {
className='interactive-editor-wrapper'
data-playwright-test-label='sp-interactive-editor'
>
<Sandpack
<SandpackProvider
template={
got('tsx')
? 'react-ts'
@@ -83,15 +91,34 @@ const InteractiveEditor = ({ files }: Props) => {
},
syntax: freeCodeCampDarkSyntax
}}
options={{
editorHeight: 450,
editorWidthPercentage: 60,
showConsole: showConsole,
showConsoleButton: showConsole,
layout: layout,
showLineNumbers: true
}}
/>
>
<SandpackLayout className='interactive-layout'>
<SandpackStack className='interactive-editor-column'>
{files.length > 1 && <FileTabs />}
<CustomMonacoEditor />
</SandpackStack>
<SandpackStack className='interactive-preview-column'>
{layout === 'preview' ? (
showConsole ? (
<>
<SandpackPreview style={{ flex: 1.5 }} />
<SandpackConsole
style={{
flex: 1,
overflow: 'scroll'
}}
/>
</>
) : (
<SandpackPreview />
)
) : (
<SandpackConsole standalone={true} />
)}
</SandpackStack>
</SandpackLayout>
</SandpackProvider>
</div>
);
};