openpencil/src/app/code/dom-preview.ts
Danila Poyarkov 6193b6da7e
feat(code): add live JSX and HTML/CSS editing (#525)
* feat(code): isolate Design JSX execution

- Transform JSX with the existing Sucrase dependency and execute it in a disposable opaque-origin iframe worker

- Block ambient network capabilities and enforce source, timeout, output, depth, and element limits

- Validate that only bounded plain structured data returns to the application

* feat(code): add editable Design JSX

- Add a lazy CodeMirror editor with JSX syntax support, completion, diagnostics, and bounded scrolling

- Convert validated sandbox output into trusted Design JSX helpers before rendering

- Apply or insert JSX as one undoable graph transaction while preserving dirty drafts

* feat(code): localize JSX editor actions

- Add translated-message fallbacks for editing, applying, inserting, and draft state

- Document the editable JSX workflow in the unreleased changelog

* fix(code): satisfy typed sandbox validation

- Keep the sandbox document terminator literal and preserve optional selection handling under type-aware lint

* test(code): resolve sandbox probes through app aliases

* fix(code): bound sandbox results before cloning

- Enforce string, array, object, depth, element, and byte limits inside the disposable worker

- Retain host-side validation as a second structured-data boundary

* fix(code): keep JSX and HTML editing modes separate

- Switch generated Tailwind output back to OpenPencil JSX before editing

- Close the JSX editor when opening HTML/CSS import and avoid stacking both editors

* feat(code): support authored Design JSX programs

- Allow local constants, function components, arrays, conditionals, fragments, and multiple roots

- Preserve replacement positions for multiple selected roots and reject mixed-parent or locked selections

- Cover multi-root undo, redo, and all-or-nothing rollback

* feat(code): add explicit JSX view mode

- Let authors leave CodeMirror without applying a draft

- Keep the editable surface and HTML/CSS importer mutually exclusive

* feat(code): diagnose unknown JSX vocabulary

- Warn on OpenPencil elements and properties that are absent from the canonical schema

- Surface diagnostics inline through CodeMirror lint markers

* test(code): accept WebKit isolation diagnostics

- Cover the sandbox architecture against Playwright WebKit

- Accept engine-specific wording while preserving the same unavailable-window assertion

* refactor(code): consolidate Design JSX vocabulary

- Drive renderer warnings, completion, and diagnostics from one supported-property schema

- Recognize locally declared components and add focused schema and transform tests

- Replace CodeMirror basicSetup with an explicit feature set and remove the umbrella package

* fix(code): support Design JSX variable helpers

* refactor(code): unify JSX sandbox validation

* fix(code): account for complete sandbox output

* fix(code): preserve locked JSX descendants

* fix(code): preserve JSX sibling order

* fix(code): recompute JSX parent layouts

* feat(code): add live code previews

* test(code): centralize graph assertions

* fix(code): harden live preview sessions

* docs: describe live code editing

* fix(code): update editor accessibility labels

* fix(code): use theme-aware error colors

* fix(dev): stop Vite disconnect error loops

* fix(code): clarify live preview status

* fix(ui): avoid tooltip attribute warnings

* test(code): assert semantic preview status

* refactor(code): remove obsolete editor messages

* fix(code): harden preview concurrency and isolation

* fix(code): cancel stale reset previews
2026-08-15 09:48:42 +03:00

62 lines
1.8 KiB
TypeScript

import { browserHTMLToSceneGraph } from '@open-pencil/dom-css/browser'
import type { SceneGraph } from '@open-pencil/scene-graph'
import type { EditorStore } from '@/app/editor/active-store'
export type DOMCodeSession = {
originalGraph: SceneGraph
originalPageId: string
previewGraph: SceneGraph | null
previewPageId: string | null
}
export function createDOMCodeSession(store: EditorStore): DOMCodeSession {
return {
originalGraph: store.graph,
originalPageId: store.state.currentPageId,
previewGraph: null,
previewPageId: null
}
}
export async function previewDOMCode(
store: EditorStore,
session: DOMCodeSession,
source: string
): Promise<{ ok: true } | { ok: false; error: string }> {
try {
const graph = await browserHTMLToSceneGraph(source, { pageName: 'Code preview' })
const pageId = graph.getPages()[0]?.id ?? graph.rootId
session.previewGraph = graph
session.previewPageId = pageId
store.replaceGraph(graph)
return { ok: true }
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) }
}
}
export function resetDOMCodePreview(store: EditorStore, session: DOMCodeSession): void {
store.replaceGraph(session.originalGraph)
void store.switchPage(session.originalPageId)
session.previewGraph = null
session.previewPageId = null
}
export function commitDOMCodeSession(store: EditorStore, session: DOMCodeSession): void {
const after = session.previewGraph
const afterPageId = session.previewPageId
if (!after || !afterPageId) return
store.pushUndoEntry({
label: 'Edit HTML/CSS',
forward: () => {
store.replaceGraph(after)
void store.switchPage(afterPageId)
},
inverse: () => {
store.replaceGraph(session.originalGraph)
void store.switchPage(session.originalPageId)
}
})
}