feat(sdk): attachSync mirrors composition edits onto a live document

Adds attachSync(comp) to PreviewAdapter/IframePreviewAdapter — does an
immediate full sync via the existing applyOverrideSet, then subscribes to
comp.on('patch', ...) and replays every future patch (forward or inverse —
undo/redo included) via the existing applyPatchesToDocument, pointed at the
iframe's live document instead of the offscreen linkedom one. No new
mutation logic; both functions already work against any
{document, wrapped, stamped}-shaped object.

Also adds a no-op attachSync stub to HeadlessPreviewAdapter, required to
keep it satisfying the widened PreviewAdapter interface.

Closes the gap that made pacific's canvas-react hand-roll its own
override-application code (applyOverrideToIframe.ts) with two separate
mechanisms (diffing for normal edits, verbatim op-replay for undo/redo) —
subscribing to the patch stream directly needs only one.
This commit is contained in:
Vance Ingalls
2026-07-09 11:52:13 -07:00
parent bcda11aa7c
commit 91ddf9e196
4 changed files with 158 additions and 2 deletions
+30 -1
View File
@@ -37,7 +37,8 @@ import {
readCurrentTranslate,
} from "@hyperframes/core/runtime/position-edits";
import type { PreviewAdapter, ElementAtPointResult, DraftProps } from "./types.js";
import type { EditOp } from "../types.js";
import type { EditOp, Composition } from "../types.js";
import { applyPatchesToDocument, applyOverrideSet } from "../engine/apply-patches.js";
// ─── Pure resolver (testable without a browser) ───────────────────────────────
@@ -511,6 +512,9 @@ class IframePreviewAdapter implements PreviewAdapter {
*/
private _draftPrevInlineTranslate: string | null = null;
/** Unsubscribe for the current attachSync subscription, if any. */
private _syncDetach: (() => void) | null = null;
constructor(iframe: HTMLIFrameElement, dispatch?: (op: EditOp) => void) {
this.iframe = iframe;
this._dispatch = dispatch;
@@ -740,6 +744,31 @@ class IframePreviewAdapter implements PreviewAdapter {
const ids = [...this._selection];
for (const h of this._handlers) h(ids);
}
/**
* Mirror `comp`'s edits onto this.iframe.contentDocument. See the
* PreviewAdapter interface doc for the full contract.
*/
attachSync(comp: Composition): () => void {
this._syncDetach?.();
const doc = this.iframe.contentDocument;
if (doc) {
applyOverrideSet({ document: doc, wrapped: false, stamped: "" }, comp.getOverrides());
}
const unsubscribe = comp.on("patch", ({ patches }) => {
const liveDoc = this.iframe.contentDocument;
if (!liveDoc) return;
applyPatchesToDocument(
{ document: liveDoc, wrapped: false, stamped: "" },
patches.filter((p) => p.path !== "/script/gsap"),
);
});
this._syncDetach = unsubscribe;
return unsubscribe;
}
}
export function createIframePreviewAdapter(