feat(frontend): Svelte editor, live preview and Git panel
- CodeMirror 6 editor with a debounced Mermaid 11 preview (zoom, pan, auto fit-to-view and inline error reporting) - project start screen with recent projects, sidebar diagram explorer, toolbar and a Git panel (status, commit, history, branches) - SVG/PNG export, per-project theme switching, toasts - rune-based central store orchestrating all backend calls - view modes (Code / Split / Preview) plus a viewer mode with a slide-in quick-edit drawer (Cmd/Ctrl+E)
This commit is contained in:
107
src/lib/components/Editor.svelte
Normal file
107
src/lib/components/Editor.svelte
Normal file
@@ -0,0 +1,107 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import {
|
||||
EditorView,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
drawSelection,
|
||||
highlightSpecialChars,
|
||||
} from '@codemirror/view';
|
||||
import {
|
||||
defaultKeymap,
|
||||
history,
|
||||
historyKeymap,
|
||||
indentWithTab,
|
||||
} from '@codemirror/commands';
|
||||
import { bracketMatching, indentOnInput } from '@codemirror/language';
|
||||
import { store } from '../store.svelte';
|
||||
|
||||
let host: HTMLDivElement;
|
||||
let view: EditorView | undefined;
|
||||
|
||||
const theme = EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
height: '100%',
|
||||
fontSize: '13.5px',
|
||||
backgroundColor: 'var(--bg-input)',
|
||||
color: 'var(--text)',
|
||||
},
|
||||
'.cm-content': {
|
||||
fontFamily: 'var(--mono)',
|
||||
caretColor: 'var(--accent)',
|
||||
padding: '12px 0',
|
||||
},
|
||||
'.cm-scroller': { fontFamily: 'var(--mono)', lineHeight: '1.55' },
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'var(--bg-input)',
|
||||
color: 'var(--text-faint)',
|
||||
border: 'none',
|
||||
},
|
||||
'.cm-activeLineGutter': { backgroundColor: 'transparent', color: 'var(--text-dim)' },
|
||||
'.cm-activeLine': { backgroundColor: 'rgba(255,255,255,0.03)' },
|
||||
'&.cm-focused .cm-cursor': { borderLeftColor: 'var(--accent)' },
|
||||
'.cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection': {
|
||||
backgroundColor: 'var(--accent-soft) !important',
|
||||
},
|
||||
'.cm-matchingBracket': { backgroundColor: 'var(--accent-soft)', outline: 'none' },
|
||||
},
|
||||
{ dark: true }
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
view = new EditorView({
|
||||
parent: host,
|
||||
state: EditorState.create({
|
||||
doc: store.content,
|
||||
extensions: [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
highlightActiveLine(),
|
||||
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
||||
theme,
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of((u) => {
|
||||
if (u.docChanged) {
|
||||
store.content = u.state.doc.toString();
|
||||
}
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
return () => view?.destroy();
|
||||
});
|
||||
|
||||
// When a different diagram is loaded, replace the whole document. Depend only
|
||||
// on activeId; read content untracked so typing doesn't reset the editor.
|
||||
$effect(() => {
|
||||
void store.activeId; // re-run only when the selected diagram changes
|
||||
if (!view) return;
|
||||
untrack(() => {
|
||||
const incoming = store.content;
|
||||
if (incoming !== view!.state.doc.toString()) {
|
||||
view!.dispatch({
|
||||
changes: { from: 0, to: view!.state.doc.length, insert: incoming },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="editor" bind:this={host}></div>
|
||||
|
||||
<style>
|
||||
.editor {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-input);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user