mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
740a83abb3
commit
1a23938bef
@@ -17,8 +17,8 @@ describe("parseHtml", () => {
|
||||
<html>
|
||||
<body>
|
||||
<div id="stage">
|
||||
<div id="text1" data-hf-id="text1" data-start="0" data-end="5" data-name="Title"><div>Hello World</div></div>
|
||||
<div id="text2" data-hf-id="text2" data-start="2" data-end="7" data-name="Subtitle"><div>Sub</div></div>
|
||||
<div id="text1" data-start="0" data-end="5" data-name="Title"><div>Hello World</div></div>
|
||||
<div id="text2" data-start="2" data-end="7" data-name="Subtitle"><div>Sub</div></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -42,7 +42,7 @@ describe("parseHtml", () => {
|
||||
<html>
|
||||
<body>
|
||||
<div id="stage">
|
||||
<div id="comp1" data-hf-id="comp1" data-start="0" data-end="10" data-type="composition" data-composition-id="abc123">
|
||||
<div id="comp1" data-start="0" data-end="10" data-type="composition" data-composition-id="abc123">
|
||||
<iframe src="/compositions/abc123"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,9 +65,9 @@ describe("parseHtml", () => {
|
||||
<html>
|
||||
<body>
|
||||
<div id="stage">
|
||||
<video id="vid1" data-hf-id="vid1" data-start="0" data-end="10" src="video.mp4" data-name="My Video"></video>
|
||||
<audio id="aud1" data-hf-id="aud1" data-start="0" data-end="5" src="music.mp3" data-name="Music"></audio>
|
||||
<img id="img1" data-hf-id="img1" data-start="2" data-end="8" src="photo.jpg" data-name="Photo" />
|
||||
<video id="vid1" data-start="0" data-end="10" src="video.mp4" data-name="My Video"></video>
|
||||
<audio id="aud1" data-start="0" data-end="5" src="music.mp3" data-name="Music"></audio>
|
||||
<img id="img1" data-start="2" data-end="8" src="photo.jpg" data-name="Photo" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -391,7 +391,7 @@ describe("parseHtml", () => {
|
||||
<html>
|
||||
<body>
|
||||
<div id="stage">
|
||||
<div id="text1" data-hf-id="text1" data-start="0" data-end="5" data-keyframes='${keyframes}'><div>Hello</div></div>
|
||||
<div id="text1" data-start="0" data-end="5" data-keyframes='${keyframes}'><div>Hello</div></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* PreviewAdapter — stub for R7 (Task 3 implements this).
|
||||
* Exports the typed API contract so tests can import and fail on assertions
|
||||
* rather than module resolution.
|
||||
*/
|
||||
|
||||
export type DraftPayload =
|
||||
| { type: "move"; hfId: string; dx: number; dy: number }
|
||||
| { type: "resize"; hfId: string; w: number; h: number };
|
||||
@@ -20,9 +14,107 @@ export interface PreviewAdapter {
|
||||
getElementTimings(): Record<string, { start?: number; end?: number }>;
|
||||
}
|
||||
|
||||
interface GestureState {
|
||||
hfId: string;
|
||||
payload: DraftPayload;
|
||||
originalTranslate: string | undefined;
|
||||
}
|
||||
|
||||
export function createPreviewAdapter(
|
||||
_document: Document,
|
||||
_opts?: { resolvePoint?: (x: number, y: number) => Element | null },
|
||||
doc: Document,
|
||||
opts?: { resolvePoint?: (x: number, y: number) => Element | null },
|
||||
): PreviewAdapter {
|
||||
throw new Error("not implemented — Task 3");
|
||||
let gesture: GestureState | null = null;
|
||||
|
||||
function findById(hfId: string): HTMLElement | null {
|
||||
return doc.querySelector(`[data-hf-id="${hfId}"]`) as HTMLElement | null;
|
||||
}
|
||||
|
||||
function opacity(el: Element): number {
|
||||
const view = doc.defaultView;
|
||||
if (!view) return 1;
|
||||
return parseFloat(view.getComputedStyle(el).opacity) || 0;
|
||||
}
|
||||
|
||||
return {
|
||||
elementAtPoint(x, y, _opts) {
|
||||
const hit = opts?.resolvePoint?.(x, y) ?? null;
|
||||
if (!hit) return null;
|
||||
|
||||
let el: Element | null = hit;
|
||||
while (el && el !== doc.body) {
|
||||
if (el.hasAttribute("data-hf-id")) {
|
||||
return opacity(el) === 0 ? null : (el as HTMLElement);
|
||||
}
|
||||
// data-hf-root without data-hf-id = outermost stage root — stop
|
||||
if (el.hasAttribute("data-hf-root")) return null;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
applyDraft(payload) {
|
||||
const target = findById(payload.hfId);
|
||||
if (!target) return;
|
||||
|
||||
const originalTranslate = target.style.getPropertyValue("translate") || undefined;
|
||||
gesture = { hfId: payload.hfId, payload, originalTranslate };
|
||||
target.setAttribute("data-hf-studio-manual-edit-gesture", "true");
|
||||
|
||||
if (payload.type === "move") {
|
||||
target.style.setProperty("--hf-studio-offset-x", `${payload.dx}px`);
|
||||
target.style.setProperty("--hf-studio-offset-y", `${payload.dy}px`);
|
||||
} else {
|
||||
target.style.setProperty("--hf-studio-width", `${payload.w}px`);
|
||||
target.style.setProperty("--hf-studio-height", `${payload.h}px`);
|
||||
}
|
||||
},
|
||||
|
||||
revertDraft() {
|
||||
if (!gesture) return;
|
||||
const target = findById(gesture.hfId);
|
||||
if (target) {
|
||||
target.style.removeProperty("--hf-studio-offset-x");
|
||||
target.style.removeProperty("--hf-studio-offset-y");
|
||||
target.style.removeProperty("--hf-studio-width");
|
||||
target.style.removeProperty("--hf-studio-height");
|
||||
target.removeAttribute("data-hf-studio-manual-edit-gesture");
|
||||
if (gesture.originalTranslate !== undefined) {
|
||||
target.style.setProperty("translate", gesture.originalTranslate);
|
||||
}
|
||||
}
|
||||
gesture = null;
|
||||
},
|
||||
|
||||
commitPreview() {
|
||||
if (!gesture) return null;
|
||||
const { hfId, payload } = gesture;
|
||||
|
||||
const target = findById(hfId);
|
||||
if (target) {
|
||||
target.removeAttribute("data-hf-studio-manual-edit-gesture");
|
||||
}
|
||||
gesture = null;
|
||||
|
||||
if (payload.type === "move") {
|
||||
return { type: "moveElement", hfId, dx: payload.dx, dy: payload.dy };
|
||||
}
|
||||
return { type: "resize", hfId, width: payload.w, height: payload.h };
|
||||
},
|
||||
|
||||
getElementTimings() {
|
||||
const result: Record<string, { start?: number; end?: number }> = {};
|
||||
for (const el of Array.from(doc.querySelectorAll("[data-hf-id]"))) {
|
||||
const hfId = el.getAttribute("data-hf-id");
|
||||
if (!hfId) continue;
|
||||
const s = el.getAttribute("data-start");
|
||||
const e = el.getAttribute("data-end");
|
||||
result[hfId] = {
|
||||
start: s !== null ? parseFloat(s) : undefined,
|
||||
end: e !== null ? parseFloat(e) : undefined,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user