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
@@ -1,2 +0,0 @@
- [vega-editor reference clone](vega-editor-reference-clone.md) — local vega/editor at /Users/oleh/code/reference/vega-editor for Monaco/render/validation techniques
- [Stack: React + Zustand](stack-moving-to-react.md) — final UI stack is React + Zustand (migrated off Preact + signals at M0); doc sweep done
@@ -1,29 +0,0 @@
---
name: stack-moving-to-react
description: Astrolabe's UI stack is React + Zustand (migrated from Preact + signals at M0)
metadata:
type: project
---
The UI stack is **React + Zustand** (user decision, 2026-06-03, executed at M0 before feature
work). It started as Preact + `@preact/signals`; a brief intermediate step migrated to React +
`@preact/signals-react`, but the final call is **React + Zustand** — signals were dropped.
**Why React:** the Preact pain was React-ecosystem friction (real-React-only libraries not
cooperating with `preact/compat`) — *not* the signals model. Real React removes that whole
class of problem and makes borrowing from the reference [[vega-editor-reference-clone]] (a
React app) port directly.
**Why Zustand over keeping signals:** signals were never the problem, but switching framework
was the one cheap moment (M0, ~nothing implemented) to also pick the lowest-future-migration-risk
state lib. Zustand is idiomatic React, has first-class outside-React access
(`getState`/`setState`/`subscribe`) that fits the "logic lives in core/services, not components"
architecture, and carries no build-time transform. The signals→Zustand cost was only rewriting
unimplemented docs.
**How to apply:** Stores are `create<State>()` modules exporting a `useXStore` hook (state +
actions in one object); components read via `useXStore(selector)` (+ `useShallow` for object
selections); non-component code uses `getState()/setState()/subscribe()`; derive in selectors,
never store derived fields. See `docs/architecture/01-state-and-stores.md`. The repo-wide
Preact/signals→React/Zustand doc sweep is **done** — no stale "Preact"/"signals" wording should
remain except where it describes the sibling project Syto, Vega's own signals, or plain English.
@@ -1,27 +0,0 @@
---
name: vega-editor-reference-clone
description: Location and nature of the local vega/editor clone used as a technique reference for Astrolabe's Monaco/vega-embed/validation work
metadata:
type: reference
---
The canonical Vega-Lite editor (vega/editor) is cloned locally at
`/Users/oleh/code/reference/vega-editor` (shallow clone of `main`, HEAD 4fdbb59). It is the
reference for the "editor + renderer" wiring Astrolabe's M1/M2 call out ("mine vega-editor
for how it wires the schema"). Re-clone with `git clone --depth 1 https://github.com/vega/editor`.
Key divergences to remember when borrowing — it is **React + Redux/context**, uses
`@monaco-editor/react` + `@monaco-editor/loader` (CDN-loaded Monaco, no explicit worker
config), and — surprisingly — **does NOT use vega-embed for its live preview** (it hand-rolls
`vegaLite.compile``vega.parse``new vega.View().runAsync()`; vega-embed is imported only
for types + the exported standalone HTML). Astrolabe is React (see [[stack-moving-to-react]]) +
Zustand stores + raw `monaco-editor` + `vegaEmbed()`. Since both are now React, vega/editor's
component lifecycle patterns port fairly directly; the friction is state (their flat Redux →
our Zustand stores) and Monaco worker wiring (their CDN loader → our explicit Vite workers).
The Monaco choice (self-hosted from npm + raw API, not the CDN loader / `@monaco-editor/react`
route) is a recorded decision — see `docs/architecture/08` § Decision · Monaco integration.
The highest-value files: `src/utils/monaco.ts` (schema wiring), `src/utils/validate.ts` (ajv),
`src/utils/jsonc-parser.ts`, `src/utils/logger.ts` (LocalLogger), `src/components/renderer/renderer.tsx`
(View lifecycle), `src/components/app.tsx:188-365` (parse→validate→compile→render flow),
`src/constants/default-state.ts` (state shape). Relevant to [[]] M1/M2 of the implementation plan.