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:
2026-06-05 00:16:24 +03:00
parent 056644450c
commit ca54bb66b1
34 changed files with 1557 additions and 74 deletions
+37
View File
@@ -0,0 +1,37 @@
/**
* Chart renderer (docs/architecture/05 §2).
*
* The one place in the app that touches `vega-embed` and the chart DOM.
* Components ask it to draw a prepared spec into a node and own the returned
* handle's lifecycle: every successful embed yields a live Vega `View` that must
* be finalized before the next embed (or it leaks timers, listeners, and DOM).
*/
import vegaEmbed, { type Result as EmbedResult } from 'vega-embed';
import type { VisualizationSpec } from 'vega-embed';
import type { Config } from 'vega-lite';
export interface RenderHandle {
/** Finalize the underlying Vega view and clear the node. */
destroy(): void;
}
/** Embed a prepared spec into `node`. Non-negotiable: no actions menu, SVG output. */
export async function renderSpec(
node: HTMLElement,
spec: VisualizationSpec,
config: Config,
): Promise<RenderHandle> {
const result: EmbedResult = await vegaEmbed(node, spec, {
actions: false, // Astrolabe owns its own export/copy affordances
renderer: 'svg',
config,
});
return {
destroy() {
result.view.finalize();
node.replaceChildren();
},
};
}