refactor: introduce codemirror for CSS editor (#2039)

* refactor: introduce codemirror for CSS editor

* chore: fix formatting
This commit is contained in:
Carlos Valente
2026-03-26 15:25:32 +01:00
committed by GitHub
parent 2331205c93
commit 3a0df0b491
10 changed files with 247 additions and 105 deletions
-4
View File
@@ -1,4 +0,0 @@
declare module 'virtual:prismjs' {
const prism: typeof import('prismjs');
export default prism;
}
@@ -1,5 +1,13 @@
.wrapper {
min-height: 500px;
max-height: 500px;
overflow-y: auto;
.editor {
width: 100%;
height: 100%;
:global(.cm-editor) {
height: 100%;
}
:global(.cm-scroller) {
height: 100%;
overflow: auto;
}
}
@@ -1,38 +1,86 @@
import { memo } from 'react';
import Editor from 'react-simple-code-editor';
import 'prismjs/themes/prism-tomorrow.min.css';
import Prism from 'virtual:prismjs';
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
import { css } from '@codemirror/lang-css';
import { Annotation, EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { vscodeDark } from '@uiw/codemirror-theme-vscode';
import { memo, useEffect, useRef } from 'react';
import style from './StyleEditor.module.scss';
interface CodeEditorProps {
language: string;
interface StyleEditorProps {
value: string;
onChange: (value: string) => void;
}
export default memo(CodeEditor);
function CodeEditor({ language, onChange, value }: CodeEditorProps) {
const highlight = (code: string) => {
const grammar = Prism.languages[language];
return grammar ? Prism.highlight(code, grammar, language) : code;
};
const editorExtensions = [keymap.of([indentWithTab, ...defaultKeymap]), css(), vscodeDark];
const externalValueSync = Annotation.define<boolean>();
return (
<div className={style.wrapper}>
<Editor
value={value}
padding={15}
onValueChange={onChange}
highlight={highlight}
style={{
fontFamily: 'monospace',
fontSize: 12,
minHeight: 500,
background: 'transparent',
}}
/>
</div>
);
export default memo(StyleEditor);
function StyleEditor({ onChange, value }: StyleEditorProps) {
const hostRef = useRef<HTMLDivElement | null>(null);
const viewRef = useRef<EditorView | null>(null);
const onChangeRef = useRef(onChange);
// Keep the latest callback available to the editor listener without recreating the editor instance.
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
// Create the CodeMirror editor once and wire document changes back to React state.
useEffect(() => {
if (viewRef.current) {
return;
}
const parent = hostRef.current;
if (!parent) {
return;
}
const updateListener = EditorView.updateListener.of((update) => {
if (!update.docChanged) {
return;
}
if (update.transactions.some((transaction) => transaction.annotation(externalValueSync))) {
return;
}
onChangeRef.current(update.state.doc.toString());
});
const state = EditorState.create({
doc: value,
extensions: [...editorExtensions, updateListener],
});
const view = new EditorView({ state, parent });
viewRef.current = view;
return () => {
view.destroy();
viewRef.current = null;
};
}, []);
// Apply external value updates to the editor when they differ from the current document.
useEffect(() => {
const view = viewRef.current;
if (!view) {
return;
}
const currentValue = view.state.doc.toString();
if (currentValue === value) {
return;
}
if (view.hasFocus) {
return;
}
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: value },
annotations: externalValueSync.of(true),
});
}, [value]);
return <div className={style.editor} ref={hostRef} />;
}
@@ -16,7 +16,7 @@
.editorBody {
position: relative;
min-height: 500px;
height: 500px;
background-color: $gray-1350;
border: 1px solid $gray-1100;
border-radius: 3px;
@@ -51,11 +51,11 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
const clear = () => setDraftCss('');
// Load the latest CSS from the server whenever the modal opens, and ignore stale responses on close.
useEffect(() => {
let isCancelled = false;
async function fetchServerCSS() {
// check for isOpen to fetch recent css
if (isOpen) {
try {
setError(null);
@@ -97,11 +97,9 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
showBackdrop
bodyElements={
<div className={style.editorBody}>
{!isLoadingCss && (
<Suspense fallback={<Panel.Loader isLoading />}>
<CodeEditor value={draftCss} onChange={setDraftCss} language='css' />
</Suspense>
)}
<Suspense fallback={<Panel.Loader isLoading />}>
<CodeEditor value={draftCss} onChange={setDraftCss} />
</Suspense>
<Panel.Loader isLoading={isLoadingCss} />
</div>
}