diff --git a/packages/sdk/src/session.test.ts b/packages/sdk/src/session.test.ts
index fd356a6ff..78fa70fff 100644
--- a/packages/sdk/src/session.test.ts
+++ b/packages/sdk/src/session.test.ts
@@ -4,6 +4,7 @@
import { describe, it, expect } from "vitest";
import { openComposition } from "./session.js";
+import type { DraftProps, ElementAtPointResult, PreviewAdapter } from "./adapters/types.js";
const BASE_HTML = `
@@ -13,6 +14,88 @@ const BASE_HTML = `
`.trim();
+class TestPreviewAdapter implements PreviewAdapter {
+ private selectionHandlers: Array<(ids: string[]) => void> = [];
+
+ elementAtPoint(_x: number, _y: number, _opts?: { atTime?: number }): ElementAtPointResult | null {
+ return null;
+ }
+
+ applyDraft(_id: string, _props: DraftProps): void {
+ // Test adapter tracks selection only.
+ }
+
+ commitPreview(): void {
+ // Test adapter tracks selection only.
+ }
+
+ cancelPreview(): void {
+ // Test adapter tracks selection only.
+ }
+
+ select(ids: string[], _opts?: { additive?: boolean }): void {
+ this.emitSelection(ids);
+ }
+
+ on(_event: "selection", handler: (ids: string[]) => void): () => void {
+ this.selectionHandlers.push(handler);
+ return () => {
+ this.selectionHandlers = this.selectionHandlers.filter((h) => h !== handler);
+ };
+ }
+
+ emitSelection(ids: readonly string[]): void {
+ const snapshot = [...ids];
+ for (const handler of this.selectionHandlers) {
+ handler([...snapshot]);
+ }
+ }
+
+ listenerCount(): number {
+ return this.selectionHandlers.length;
+ }
+}
+
+// ─── Preview selection bridge ────────────────────────────────────────────────
+
+describe("preview selection bridge", () => {
+ it("mirrors preview selection into session state and notifies subscribers", async () => {
+ const preview = new TestPreviewAdapter();
+ const comp = await openComposition(BASE_HTML, { preview });
+ const events: string[][] = [];
+
+ comp.on("selectionchange", (ids) => events.push([...ids]));
+ preview.select(["hf-title"]);
+
+ expect(comp.getSelection()).toEqual(["hf-title"]);
+ expect(comp.selection().ids).toEqual(["hf-title"]);
+ expect(events).toEqual([["hf-title"]]);
+ });
+
+ it("selection proxy applies edits to ids selected by the preview", async () => {
+ const preview = new TestPreviewAdapter();
+ const comp = await openComposition(BASE_HTML, { preview });
+
+ preview.select(["hf-title", "hf-sub"]);
+ comp.selection().setStyle({ color: "#123456" });
+
+ expect(comp.getElement("hf-title")?.inlineStyles["color"]).toBe("#123456");
+ expect(comp.getElement("hf-sub")?.inlineStyles["color"]).toBe("#123456");
+ });
+
+ it("dispose unsubscribes from preview selection events", async () => {
+ const preview = new TestPreviewAdapter();
+ const comp = await openComposition(BASE_HTML, { preview });
+
+ expect(preview.listenerCount()).toBe(1);
+ comp.dispose();
+ expect(preview.listenerCount()).toBe(0);
+
+ preview.select(["hf-title"]);
+ expect(comp.getSelection()).toEqual([]);
+ });
+});
+
// ─── History coalescing ───────────────────────────────────────────────────────
describe("history coalescing", () => {
diff --git a/packages/sdk/src/session.ts b/packages/sdk/src/session.ts
index 989992a48..3a0a3ab44 100644
--- a/packages/sdk/src/session.ts
+++ b/packages/sdk/src/session.ts
@@ -66,6 +66,7 @@ class CompositionImpl implements Composition {
private selectionHandlers: Array<(ids: string[]) => void> = [];
private patchHandlers: Array<(e: PatchEvent) => void> = [];
private errorHandlers: Array<(e: PersistErrorEvent) => void> = [];
+ private previewSelectionUnsubscribe: (() => void) | null = null;
/** Attached by openComposition() for standalone mode. */
private historyModule: HistoryModule | null = null;
@@ -85,6 +86,8 @@ class CompositionImpl implements Composition {
this.persist = opts.persist;
this.preview = opts.preview;
this.overrides = { ...(opts.overrides ?? {}) };
+ this.previewSelectionUnsubscribe =
+ this.preview?.on("selection", (ids) => this.updateSelection(ids)) ?? null;
}
attachHistory(module: HistoryModule): void {
@@ -206,6 +209,13 @@ class CompositionImpl implements Composition {
return [...this.currentSelection];
}
+ private updateSelection(ids: readonly string[]): void {
+ this.currentSelection = [...ids];
+ for (const handler of this.selectionHandlers) {
+ handler([...this.currentSelection]);
+ }
+ }
+
// ── Dispatch / batch ─────────────────────────────────────────────────────────
// fallow-ignore-next-line complexity
@@ -396,6 +406,8 @@ class CompositionImpl implements Composition {
}
dispose(): void {
+ this.previewSelectionUnsubscribe?.();
+ this.previewSelectionUnsubscribe = null;
this.persistQueueModule?.dispose();
this.historyModule?.dispose();
this.changeHandlers = [];