Files
liqa/client/src/ui/CodeEditor/CodeEditor.tsx
T
ars9 1c13e068ad feat(ui): add CodeBlock and CodeEditor components with Monaco and hljs
- add CodeBlock for read-only syntax-highlighted display (hljs, js/json)
- add CodeEditor wrapping Monaco editor with theme sync, focus state, and error state
- replace code textareas in snippet, credential, and step pages with CodeEditor
- replace code <pre> blocks in snippet and credential detail pages with CodeBlock
- make form cards full-width on pages with code editors
- add resize:vertical support to CodeEditor wrapper with automaticLayout
2026-04-10 15:53:26 +03:00

84 lines
2.0 KiB
TypeScript

import { useState, useEffect } from 'react';
import Editor, { OnMount } from '@monaco-editor/react';
import styles from './CodeEditor.module.css';
export interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
language?: string;
rows?: number;
error?: boolean;
}
function useMonacoTheme() {
const [theme, setTheme] = useState(() =>
document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs-light'
);
useEffect(() => {
const observer = new MutationObserver(() => {
setTheme(
document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs-light'
);
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
return () => observer.disconnect();
}, []);
return theme;
}
export function CodeEditor({
value,
onChange,
language = 'javascript',
rows = 6,
error = false,
}: CodeEditorProps) {
const theme = useMonacoTheme();
const [focused, setFocused] = useState(false);
const height = rows * 20 + 16;
const handleMount: OnMount = (editor) => {
editor.onDidFocusEditorWidget(() => setFocused(true));
editor.onDidBlurEditorWidget(() => setFocused(false));
};
return (
<div
className={[
styles.wrapper,
focused ? styles.focused : '',
error ? styles.hasError : '',
]
.filter(Boolean)
.join(' ')}
style={{ height }}
>
<Editor
height="100%"
theme={theme}
language={language}
value={value}
onChange={(v) => onChange(v ?? '')}
onMount={handleMount}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
lineHeight: 20,
wordWrap: 'on',
padding: { top: 8, bottom: 8 },
folding: false,
renderLineHighlight: 'line',
scrollbar: { vertical: 'auto', horizontal: 'hidden' },
automaticLayout: true,
}}
/>
</div>
);
}