mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Implement M1 authoring loop: library, editor, live preview, persistence
M1 MVP from docs/IMPLEMENTATION-PLAN.md, with the /alignment pass applied. - core: Snippet model + factory; prepareSpecForRender (copy-not-mutate); per-theme Vega chart config - state/orchestration: SnippetStore with debounced auto-save; IndexedDB adapter + read-time migration; startup hydration + write-through persistence - ui: SnippetLibrary, SpecEditor (Monaco edcore.main — full editor features, JSON-only languages), LivePreview - build: Monaco/Vega manual chunks; raised PWA precache ceiling - alignment: flush a valid draft on snippet switch (+regression tests); TODO breadcrumbs for the preview render race and the window.confirm delete - housekeeping: gitignore .claude/projects/
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Spec Editor — the center pane (spec §03).
|
||||
*
|
||||
* M1 scope: a Monaco JSON editor bound to the active snippet's draft. It runs
|
||||
* uncontrolled (raw `monaco-editor`, per docs/architecture/08): created once,
|
||||
* keystrokes push into the store's `draftText` buffer, and the buffer is pulled
|
||||
* back in only when the active snippet *changes* (select/create/delete) — never
|
||||
* keystroke-by-keystroke, which would fight the cursor. Schema validation/
|
||||
* autocomplete, the draft/published toggle, and the inline error surface arrive
|
||||
* in M2.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
// `edcore.main` is the full standalone editor — every feature contribution
|
||||
// (folding, suggest widget, word operations like Cmd+Backspace, find, bracket
|
||||
// colorization, multi-cursor, …) — but WITHOUT the `monaco-editor` barrel's
|
||||
// basic-languages (sql, abap, solidity, …) we never use. We then add only the
|
||||
// JSON language service. Full editor UX, JSON-only weight.
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/edcore.main';
|
||||
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
|
||||
import '../infrastructure/monaco-env'; // side-effect: wire workers before create
|
||||
import { configureVegaLiteJson } from '../infrastructure/monaco-schema';
|
||||
import { useAppStore } from '../stores/AppStore';
|
||||
import { useSnippetStore } from '../stores/SnippetStore';
|
||||
import styles from './SpecEditor.module.css';
|
||||
|
||||
// Register the bundled Vega-Lite schema once: resolves `$schema` locally (no
|
||||
// network warning) and powers validation, autocomplete, and hover docs.
|
||||
configureVegaLiteJson();
|
||||
|
||||
export function SpecEditor() {
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||
const activeId = useSnippetStore((s) => s.activeSnippetId);
|
||||
const uiTheme = useAppStore((s) => s.uiTheme);
|
||||
|
||||
// Create the editor once, on mount.
|
||||
useEffect(() => {
|
||||
if (!hostRef.current) return;
|
||||
const editor = monaco.editor.create(hostRef.current, {
|
||||
value: useSnippetStore.getState().draftText,
|
||||
language: 'json',
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
tabSize: 2,
|
||||
wordWrap: 'on',
|
||||
folding: true,
|
||||
showFoldingControls: 'always', // keep fold arrows visible, not only on hover
|
||||
scrollBeyondLastLine: false,
|
||||
// Vega-Lite enum values ("bar", "quantitative", …) live inside JSON
|
||||
// strings, where Monaco disables auto-suggest by default — turn it on so
|
||||
// those keywords are hinted as you type, not just on Ctrl+Space.
|
||||
quickSuggestions: { other: true, comments: false, strings: true },
|
||||
suggestOnTriggerCharacters: true,
|
||||
});
|
||||
editorRef.current = editor;
|
||||
|
||||
const sub = editor.onDidChangeModelContent(() => {
|
||||
useSnippetStore.getState().updateDraft(editor.getValue());
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.dispose();
|
||||
editor.dispose();
|
||||
editorRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Replace the buffer when the active snippet changes (not while typing).
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
const text = useSnippetStore.getState().draftText;
|
||||
if (editor.getValue() !== text) editor.setValue(text);
|
||||
editor.updateOptions({ readOnly: activeId === null });
|
||||
}, [activeId]);
|
||||
|
||||
// Editor theme follows the UI theme (M1: light/dark stock themes).
|
||||
useEffect(() => {
|
||||
monaco.editor.setTheme(uiTheme === 'experimental' ? 'vs-dark' : 'vs');
|
||||
}, [uiTheme]);
|
||||
|
||||
return (
|
||||
<div className={styles.editorPane}>
|
||||
{activeId === null && <div className={styles.placeholder}>Select or create a snippet</div>}
|
||||
<div className={styles.editor} ref={hostRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user