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
+47
View File
@@ -0,0 +1,47 @@
/**
* Rendering contract — pure spec preparation (spec §04 → Rendering Contract).
*
* Portable core: no browser APIs, no React, no vega-embed. `prepareSpecForRender`
* is the single transform that sits between "parsed spec the user authored" and
* "spec the preview actually embeds" (see docs/architecture/05). It performs two
* deterministic steps, in order, **on a deep copy** so the user's stored spec is
* never mutated by rendering:
*
* 1. Dataset reference resolution — arrives in M3 (no-op here).
* 2. Fit-mode sizing — arrives in M2 (no-op here).
*
* In M1 it is an identity transform over a copy: it establishes the
* copy-not-mutate invariant and the call site the renderer depends on, so M2/M3
* can fill in the steps without the preview pipeline changing shape.
*/
/** Preview sizing modes (spec §04 → Fit / Sizing Modes). `default` = Original. */
export type FitMode = 'default' | 'width' | 'height' | 'full';
export interface PrepareOptions {
/** Active fit mode. Applied in M2; ignored in M1. */
fitMode?: FitMode;
}
/**
* Escape `.`/`[`/`]` so Vega-Lite treats a string as a literal field name rather
* than a nested-property accessor (docs/architecture/05 §4). Used wherever
* Astrolabe *constructs* a `field:` from a data-derived column name (chart
* builder, M4); hand-authored specs are the user's responsibility.
*/
export function escapeVegaField(name: string): string {
return name.replace(/([.[\]])/g, '\\$1');
}
/**
* Transform the authored spec into the spec to embed. Operates on a deep copy
* and returns it; the input is never mutated.
*/
export function prepareSpecForRender<T>(spec: T, _options: PrepareOptions = {}): T {
const copy = structuredClone(spec);
// M3: resolveDatasetRefs(copy, datasets)
// M2: applyFitMode(copy, options.fitMode)
return copy;
}