meatloaf-config/src/app/components/CodeEditor.tsx

118 lines
3.9 KiB
TypeScript

import { useRef, useState } from 'react';
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
import { json } from '@codemirror/lang-json';
import { xml } from '@codemirror/lang-xml';
import { oneDark } from '@codemirror/theme-one-dark';
import { Eye, Pencil, Save } from 'lucide-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
export type CodeMode = 'text' | 'json' | 'xml';
interface CodeEditorProps {
text: string;
mode: CodeMode;
readOnly?: boolean;
onSave?: (text: string) => Promise<void>;
}
const cmTheme = EditorView.theme({
'&': { height: '100%', background: '#0a0a0a' },
'.cm-scroller': { overflow: 'auto', fontFamily: 'ui-monospace,monospace', fontSize: '12px', lineHeight: '1.5' },
'.cm-content': { padding: '12px 0' },
'.cm-focused': { outline: 'none' },
});
const langExt: Record<CodeMode, any> = {
json: json(),
xml: xml(),
text: [],
};
const syntaxLang: Record<CodeMode, string> = {
text: 'text', json: 'json', xml: 'xml',
};
function prettify(text: string, mode: CodeMode): string {
if (mode === 'json') {
try { return JSON.stringify(JSON.parse(text), null, 2); } catch { /* fall through */ }
}
return text;
}
export default function CodeEditor({ text, mode, readOnly = false, onSave }: CodeEditorProps) {
const [editMode, setEditMode] = useState(false);
const [saving, setSaving] = useState(false);
const editorViewRef = useRef<EditorView | null>(null);
const displayText = prettify(text, mode);
const extensions = [langExt[mode], cmTheme].flat();
const handleSave = async () => {
if (!editorViewRef.current || !onSave) return;
setSaving(true);
try { await onSave(editorViewRef.current.state.doc.toString()); }
finally { setSaving(false); }
};
if (!editMode) {
return (
<div className="flex flex-col h-full">
{!readOnly && (
<div className="flex items-center gap-2 px-3 py-1.5 bg-neutral-900 border-b border-neutral-700 flex-shrink-0 text-xs">
<button
onClick={() => setEditMode(true)}
className="px-2 py-1 rounded bg-neutral-700 text-neutral-300 hover:bg-neutral-600 inline-flex items-center gap-1"
>
<Pencil className="w-3.5 h-3.5" /> Edit
</button>
</div>
)}
<div className="flex-1 overflow-auto text-xs">
<SyntaxHighlighter
language={syntaxLang[mode]}
style={vscDarkPlus}
customStyle={{ margin: 0, minHeight: '100%', background: '#0a0a0a', fontSize: '12px', lineHeight: '1.5' }}
showLineNumbers
wrapLongLines
>
{displayText}
</SyntaxHighlighter>
</div>
</div>
);
}
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-2 px-3 py-1.5 bg-neutral-900 border-b border-neutral-700 flex-shrink-0 text-xs">
<button
onClick={() => setEditMode(false)}
className="px-2 py-1 rounded bg-amber-600 text-white inline-flex items-center gap-1"
>
<Eye className="w-3.5 h-3.5" /> View
</button>
{onSave && (
<button
onClick={() => void handleSave()}
disabled={saving}
className="px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 inline-flex items-center gap-1"
>
<Save className="w-3.5 h-3.5" /> {saving ? 'Saving…' : 'Save'}
</button>
)}
<span className="text-neutral-600 ml-auto">Ctrl+Z/Y undo · Ctrl+F search</span>
</div>
<div className="flex-1 overflow-hidden">
<CodeMirror
defaultValue={displayText}
extensions={extensions}
theme={oneDark}
height="100%"
onCreateEditor={view => { editorViewRef.current = view; }}
/>
</div>
</div>
);
}