Skip to content

Commit 1a23938

Browse files
vanceingallsclaude
andauthored
feat(core): implement createPreviewAdapter (R7, Task 3) (#1291)
* test(core): data-hf-id survives id/selector patch (R1, T7) Locks the preservation guarantee the write-back design depends on: a Studio edit targeting by id or selector (it never sends hfId) must not strip an existing data-hf-id, or the stable handle is destroyed by the next edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): escape hfId in selector + warn on duplicate match (R1, T7 review) Addresses review on #1272 (Miguel P3 + Rames): findTargetElement interpolated target.hfId raw into a [data-hf-id="..."] selector. Escape it (CSS attr-value injection guard) and warn when a hfId matches more than one element instead of silently patching an arbitrary one. Adds an injection-guard test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core): implement createPreviewAdapter — greens 20 T10 tests (R7, Task 3) elementAtPoint: resolvePoint callback → walk ancestors for data-hf-id, skip data-hf-root without data-hf-id (stage root), skip opacity-0 elements. applyDraft: find element by hfId, record originalTranslate, set --hf-studio-offset-x/y (move) or --hf-studio-width/height (resize), mark data-hf-studio-manual-edit-gesture. revertDraft: remove draft CSS props, clear gesture marker, restore originalTranslate if one was recorded. commitPreview: extract patch (move→moveElement, resize→resize with w/h renamed to width/height), clear gesture marker, return patch or null. getElementTimings: scan [data-hf-id] elements, parse data-start/data-end as floats, return map with undefined fields for absent attributes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(core): remove explicit data-hf-id from htmlParser tests so ensureHfIds mints hf- ids Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 740a83a commit 1a23938

2 files changed

Lines changed: 108 additions & 16 deletions

File tree

packages/core/src/parsers/htmlParser.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ describe("parseHtml", () => {
1717
<html>
1818
<body>
1919
<div id="stage">
20-
<div id="text1" data-hf-id="text1" data-start="0" data-end="5" data-name="Title"><div>Hello World</div></div>
21-
<div id="text2" data-hf-id="text2" data-start="2" data-end="7" data-name="Subtitle"><div>Sub</div></div>
20+
<div id="text1" data-start="0" data-end="5" data-name="Title"><div>Hello World</div></div>
21+
<div id="text2" data-start="2" data-end="7" data-name="Subtitle"><div>Sub</div></div>
2222
</div>
2323
</body>
2424
</html>
@@ -42,7 +42,7 @@ describe("parseHtml", () => {
4242
<html>
4343
<body>
4444
<div id="stage">
45-
<div id="comp1" data-hf-id="comp1" data-start="0" data-end="10" data-type="composition" data-composition-id="abc123">
45+
<div id="comp1" data-start="0" data-end="10" data-type="composition" data-composition-id="abc123">
4646
<iframe src="/compositions/abc123"></iframe>
4747
</div>
4848
</div>
@@ -65,9 +65,9 @@ describe("parseHtml", () => {
6565
<html>
6666
<body>
6767
<div id="stage">
68-
<video id="vid1" data-hf-id="vid1" data-start="0" data-end="10" src="video.mp4" data-name="My Video"></video>
69-
<audio id="aud1" data-hf-id="aud1" data-start="0" data-end="5" src="music.mp3" data-name="Music"></audio>
70-
<img id="img1" data-hf-id="img1" data-start="2" data-end="8" src="photo.jpg" data-name="Photo" />
68+
<video id="vid1" data-start="0" data-end="10" src="video.mp4" data-name="My Video"></video>
69+
<audio id="aud1" data-start="0" data-end="5" src="music.mp3" data-name="Music"></audio>
70+
<img id="img1" data-start="2" data-end="8" src="photo.jpg" data-name="Photo" />
7171
</div>
7272
</body>
7373
</html>
@@ -391,7 +391,7 @@ describe("parseHtml", () => {
391391
<html>
392392
<body>
393393
<div id="stage">
394-
<div id="text1" data-hf-id="text1" data-start="0" data-end="5" data-keyframes='${keyframes}'><div>Hello</div></div>
394+
<div id="text1" data-start="0" data-end="5" data-keyframes='${keyframes}'><div>Hello</div></div>
395395
</div>
396396
</body>
397397
</html>
Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
/**
2-
* PreviewAdapter — stub for R7 (Task 3 implements this).
3-
* Exports the typed API contract so tests can import and fail on assertions
4-
* rather than module resolution.
5-
*/
6-
71
export type DraftPayload =
82
| { type: "move"; hfId: string; dx: number; dy: number }
93
| { type: "resize"; hfId: string; w: number; h: number };
@@ -20,9 +14,107 @@ export interface PreviewAdapter {
2014
getElementTimings(): Record<string, { start?: number; end?: number }>;
2115
}
2216

17+
interface GestureState {
18+
hfId: string;
19+
payload: DraftPayload;
20+
originalTranslate: string | undefined;
21+
}
22+
2323
export function createPreviewAdapter(
24-
_document: Document,
25-
_opts?: { resolvePoint?: (x: number, y: number) => Element | null },
24+
doc: Document,
25+
opts?: { resolvePoint?: (x: number, y: number) => Element | null },
2626
): PreviewAdapter {
27-
throw new Error("not implemented — Task 3");
27+
let gesture: GestureState | null = null;
28+
29+
function findById(hfId: string): HTMLElement | null {
30+
return doc.querySelector(`[data-hf-id="${hfId}"]`) as HTMLElement | null;
31+
}
32+
33+
function opacity(el: Element): number {
34+
const view = doc.defaultView;
35+
if (!view) return 1;
36+
return parseFloat(view.getComputedStyle(el).opacity) || 0;
37+
}
38+
39+
return {
40+
elementAtPoint(x, y, _opts) {
41+
const hit = opts?.resolvePoint?.(x, y) ?? null;
42+
if (!hit) return null;
43+
44+
let el: Element | null = hit;
45+
while (el && el !== doc.body) {
46+
if (el.hasAttribute("data-hf-id")) {
47+
return opacity(el) === 0 ? null : (el as HTMLElement);
48+
}
49+
// data-hf-root without data-hf-id = outermost stage root — stop
50+
if (el.hasAttribute("data-hf-root")) return null;
51+
el = el.parentElement;
52+
}
53+
return null;
54+
},
55+
56+
applyDraft(payload) {
57+
const target = findById(payload.hfId);
58+
if (!target) return;
59+
60+
const originalTranslate = target.style.getPropertyValue("translate") || undefined;
61+
gesture = { hfId: payload.hfId, payload, originalTranslate };
62+
target.setAttribute("data-hf-studio-manual-edit-gesture", "true");
63+
64+
if (payload.type === "move") {
65+
target.style.setProperty("--hf-studio-offset-x", `${payload.dx}px`);
66+
target.style.setProperty("--hf-studio-offset-y", `${payload.dy}px`);
67+
} else {
68+
target.style.setProperty("--hf-studio-width", `${payload.w}px`);
69+
target.style.setProperty("--hf-studio-height", `${payload.h}px`);
70+
}
71+
},
72+
73+
revertDraft() {
74+
if (!gesture) return;
75+
const target = findById(gesture.hfId);
76+
if (target) {
77+
target.style.removeProperty("--hf-studio-offset-x");
78+
target.style.removeProperty("--hf-studio-offset-y");
79+
target.style.removeProperty("--hf-studio-width");
80+
target.style.removeProperty("--hf-studio-height");
81+
target.removeAttribute("data-hf-studio-manual-edit-gesture");
82+
if (gesture.originalTranslate !== undefined) {
83+
target.style.setProperty("translate", gesture.originalTranslate);
84+
}
85+
}
86+
gesture = null;
87+
},
88+
89+
commitPreview() {
90+
if (!gesture) return null;
91+
const { hfId, payload } = gesture;
92+
93+
const target = findById(hfId);
94+
if (target) {
95+
target.removeAttribute("data-hf-studio-manual-edit-gesture");
96+
}
97+
gesture = null;
98+
99+
if (payload.type === "move") {
100+
return { type: "moveElement", hfId, dx: payload.dx, dy: payload.dy };
101+
}
102+
return { type: "resize", hfId, width: payload.w, height: payload.h };
103+
},
104+
105+
getElementTimings() {
106+
const result: Record<string, { start?: number; end?: number }> = {};
107+
for (const el of Array.from(doc.querySelectorAll("[data-hf-id]"))) {
108+
const hfId = el.getAttribute("data-hf-id");
109+
if (!hfId) continue;
110+
const s = el.getAttribute("data-start");
111+
const e = el.getAttribute("data-end");
112+
result[hfId] = {
113+
start: s !== null ? parseFloat(s) : undefined,
114+
end: e !== null ? parseFloat(e) : undefined,
115+
};
116+
}
117+
return result;
118+
},
119+
};
28120
}

0 commit comments

Comments
 (0)