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
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
transition: border-color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
|
||||
.focused {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.hasError {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user