feat(editor): board shell with iframe frame and selection overlay

React Flow board (pan/zoom + frame drag are pure editor-side transforms
— zero canvas-protocol traffic, <16ms budget) hosting one iframe frame
node on the adapter origin. The frame waits for runtime.ready
(queue-drain) behind an explicit 'Starting preview…' skeleton, then
requests a single Element via render-frame (core-runner-protocol
schema) plus geometry; agent-reported geometry stays frame-local and
click reports drive store selection. The selection ring renders as an
editor-side overlay from reported geometry — chrome never inside the
customer's document (ADR-028). Stories + jsdom component tests for
board/frame/overlay; Storybook stories glob extended to
packages/editor (verified via static build).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 22:18:24 +02:00
parent c2f56ac8c9
commit 9495023aed
13 changed files with 899 additions and 4 deletions

View File

@@ -0,0 +1,31 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { SelectionOverlay } from "./selection-overlay";
describe("SelectionOverlay", () => {
it("renders nothing when rect is null (no selection)", () => {
render(<SelectionOverlay rect={null} />);
expect(screen.queryByTestId("selection-overlay")).not.toBeInTheDocument();
});
it("positions the ring from the agent-reported rect", () => {
render(
<SelectionOverlay rect={{ x: 24, y: 12, width: 320, height: 48 }} />,
);
const overlay = screen.getByTestId("selection-overlay");
expect(overlay).toHaveStyle({
position: "absolute",
left: "24px",
top: "12px",
width: "320px",
height: "48px",
});
});
it("is chrome, not content: hidden from a11y and inert to pointers (ADR-028)", () => {
render(<SelectionOverlay rect={{ x: 0, y: 0, width: 10, height: 10 }} />);
const overlay = screen.getByTestId("selection-overlay");
expect(overlay).toHaveAttribute("aria-hidden", "true");
expect(overlay).toHaveStyle({ pointerEvents: "none" });
});
});