diff --git a/packages/core/src/parsers/htmlParser.test.ts b/packages/core/src/parsers/htmlParser.test.ts index 13df9f5e0..2d786a7d3 100644 --- a/packages/core/src/parsers/htmlParser.test.ts +++ b/packages/core/src/parsers/htmlParser.test.ts @@ -17,8 +17,8 @@ describe("parseHtml", () => {
-
Hello World
-
Sub
+
Hello World
+
Sub
@@ -42,7 +42,7 @@ describe("parseHtml", () => {
-
+
@@ -65,9 +65,9 @@ describe("parseHtml", () => {
- - - + + +
@@ -391,7 +391,7 @@ describe("parseHtml", () => {
-
Hello
+
Hello
diff --git a/packages/core/src/studio-api/helpers/previewAdapter.ts b/packages/core/src/studio-api/helpers/previewAdapter.ts index 6a6b4f022..e1ecca91e 100644 --- a/packages/core/src/studio-api/helpers/previewAdapter.ts +++ b/packages/core/src/studio-api/helpers/previewAdapter.ts @@ -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; } +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 = {}; + 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; + }, + }; }