mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(studio): stabilize manual drag targets (#1393)
## Problem Studio manual drag had two bad target paths in `hf-keyframes-test`: - Drag could start from a cached hover selection instead of the selected overlay, so moving `#cta` could write an offset to the wrong element while the overlay appeared correct. - `#stage` is the visual 1920x1080 canvas, but the composition metadata lives on `<html data-composition-id ...>`. Studio treated `#stage` as a normal movable layer, so dragging it moved the coordinate basis the overlay depends on and left Undo with no reliable saved edit to reverse. ## Fix - Manual drag now starts only from the selected overlay bounds; canvas pointer-down no longer starts movement from `hoverSelectionRef`. - Added a structural root-layer heuristic: a direct body child matching the composition root's declared `data-width` / `data-height` is selectable and style-editable, but cannot receive manual offset/size/rotation edits. - Manual geometry commits now return/rethrow the save promise so failed writes roll back optimistic DOM changes instead of silently sticking. - GSAP-targeted CSS fallback edits are blocked before applying manual geometry when the GSAP drag intercept is unavailable. ## Tests - New regression coverage for stale-hover drag start and full-canvas root-stage capability resolution. - Focused DOM-edit suite: 119/119 pass. - Full Studio suite: 800 pass, 18 todo. - Studio typecheck clean; oxlint/oxfmt clean; lefthook pre-commit clean. - Studio build passes with the existing Vite chunk-size warning. - Browser verified with `agent-browser`: attempted `#stage` drag writes no offset and keeps Undo disabled; `#cta` drag writes exactly one offset to `#cta`; Undo returns the fixture to zero offsets. Local browser artifacts: `artifacts/studio-cta-drag/stage-cta-drag-fix.webm`, `artifacts/studio-cta-drag/stage-cta-drag-fix-final.png`.
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
getDirectLayerChildren,
|
||||
getSelectionCandidate,
|
||||
} from "./domEditingElement";
|
||||
import { isCompositionRootLayer } from "./domEditingRootLayer";
|
||||
|
||||
// ─── Text fields ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -179,6 +180,7 @@ export function resolveDomEditCapabilities(args: {
|
||||
inlineStyles: Record<string, string>;
|
||||
computedStyles: Record<string, string>;
|
||||
isCompositionHost: boolean;
|
||||
isCompositionRoot?: boolean;
|
||||
isInsideLockedComposition: boolean;
|
||||
isMasterView: boolean;
|
||||
existsInSource?: boolean;
|
||||
@@ -211,6 +213,19 @@ export function resolveDomEditCapabilities(args: {
|
||||
};
|
||||
}
|
||||
|
||||
if (args.isCompositionRoot) {
|
||||
return {
|
||||
canSelect: true,
|
||||
canEditStyles: true,
|
||||
canMove: false,
|
||||
canResize: false,
|
||||
canApplyManualOffset: false,
|
||||
canApplyManualSize: false,
|
||||
canApplyManualRotation: false,
|
||||
reasonIfDisabled: "The root composition defines the preview bounds.",
|
||||
};
|
||||
}
|
||||
|
||||
const position = args.computedStyles.position;
|
||||
const left = parsePx(args.inlineStyles.left) ?? parsePx(args.computedStyles.left);
|
||||
const top = parsePx(args.inlineStyles.top) ?? parsePx(args.computedStyles.top);
|
||||
@@ -341,6 +356,9 @@ export async function resolveDomEditSelection(
|
||||
undefined;
|
||||
const inlineStyles = getInlineStyles(current);
|
||||
const computedStyles = getCuratedComputedStyles(current);
|
||||
const isCompositionRoot =
|
||||
(current.hasAttribute("data-composition-id") && !compositionSrc) ||
|
||||
isCompositionRootLayer(current, doc, computedStyles);
|
||||
const textFields = collectDomEditTextFields(current);
|
||||
const isInsideLocked = Boolean(findClosestByAttribute(current, ["data-timeline-locked"]));
|
||||
let existsInSource: boolean | undefined;
|
||||
@@ -361,6 +379,7 @@ export async function resolveDomEditSelection(
|
||||
inlineStyles,
|
||||
computedStyles,
|
||||
isCompositionHost: Boolean(compositionSrc),
|
||||
isCompositionRoot,
|
||||
isInsideLockedComposition: isInsideLocked,
|
||||
isMasterView: options.isMasterView,
|
||||
existsInSource,
|
||||
|
||||
Reference in New Issue
Block a user