mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio,core): persist manual position edits for GSAP-owned elements (#1346)
* feat(sdk): scaffold @hyperframes/sdk — engine layer (model, RFC 6902 patches, mutate, apply-patches) * fix(sdk): make engine-layer PR self-contained — trim index.ts, guard indexed access - index.ts no longer exports document/session/history/persist-queue (those modules land in the next stacked PR); branch now typechecks standalone - setOwnText: optional-chain children[i] access (TS2532 under noUncheckedIndexedAccess) - fallow suppressions for buildPatchEvent + adapters/types.ts — consumers arrive in #1325 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline - applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9 parser-backed ops instead of silently no-opping — callers must never believe an animation edit succeeded when nothing was mutated - validateOp returns false for Phase 3b ops so can() feature-detects - root package.json build filter now includes @hyperframes/sdk (package is dist-only; top-level build previously produced no SDK artifacts). publish.yml intentionally NOT updated — sdk stays unpublished until Phase 3 completes. Adversarial-review findings F3 + F4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): cross-realm origin sentinel, dual width/height channel, contract docs Round-2 review (Rames/Miguel) on the engine layer: - ORIGIN_APPLY_PATCHES: unique symbol → namespaced string ('@hyperframes/sdk:applyPatches'). Symbols are realm-local — they don't survive postMessage/structured-clone, which T3 embedded hosts may forward patch events across. Namespaced string keeps collision risk negligible. - setCompositionMetadata width/height: runtime treats data-width/data-height as a forced override of inline style (init.ts applyCompositionSizing). Style is always written; the data-* attr is updated when already present so the edit isn't clobbered on load. Absent attrs stay absent — inverses stay exact. Mirrored in the patch applier; 3 new tests. - JsonPatchOp documented as the emit-only RFC 6902 subset (add/remove/replace); applier header notes move/copy/test are ignored. - SdkDocument.html documented as a build-time snapshot (serialize() is the live state). - patches.ts path-grammar comment fixed: timing/{start|end|trackIndex}. NOT changed (with reasons, see PR reply): moveElement left/top matches Studio's own inline-style commit convention (sourcePatcher); package version follows the repo-wide single-version policy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): moveElement writes data-x/data-y, not left/top CSS HF elements use data-x/data-y for positioning (read by htmlParser.ts, emitted by hyperframes generator). CSS left/top is not the runtime convention. Adds inverse round-trip test for prior position restore. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update bun.lock after sdk package registration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete * fix(sdk): address review — live-DOM query cache, single parse, style parse dedup - getElements/getElement/find now walk the live linkedom DOM via buildRoots with a lazily-built cache invalidated on dispatch/applyPatches — no serialize→ensureHfIds→parseHTML round trip per query - openComposition parses once (parseMutable); dropped discarded _doc constructor param and the redundant buildDocument call - document.ts buildElement reuses model.ts getElementStyles — removes duplicated parseInlineStyles (also fixes custom-prop camelCase mangling) - JSDoc note: empty batch() still fires change handlers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): restore full public exports now session/document modules exist index.ts re-exports document/session/history/persist-queue (trimmed in the engine-layer PR to keep it self-contained); drops the temporary fallow suppressions whose consumers now exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): coalesce history by patch paths; replay override-set on open Adversarial-review findings F1 + F2: - history: coalescing now requires identical patch paths in addition to op types + origin + window. Previously two rapid setStyle calls on DIFFERENT elements merged into one entry carrying the second forward + first inverse — undo then reverted the wrong element and stranded the latest edit. Slider drags on one property still coalesce. - T3 init: openComposition({ overrides }) now replays the stored override-set onto the freshly-parsed base before exposing the session (new keyToPath inverse mapping + applyOverrideSet). Previously the overrides were copied into the map but never applied — reopening an embedded composition showed and serialized the base template. - examples: GSAP calls now feature-detect with can() (Phase 3b ops throw UnsupportedOpError as of the engine-layer fix); UnsupportedOpError re-exported from the package entry. - 8 new session tests: coalesce same-path / cross-element / cross-prop, override round-trip (style/text/attr/timing/removal/restore-base). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify Round-2 review (Rames/Miguel) on the session layer: - batch() is now transactional: on throw, accumulated inverse patches are replayed in reverse and the override-set snapshot restored — the model is exactly as it was at batch entry. Previously a throwing batch left the DOM partially mutated with no patch trail, no history entry, no recovery path. 2 new tests (model unchanged + undo is no-op after throwing batch). - history coalesce key sorts opTypes — same op-type set coalesces regardless of dispatch order within a batch. - applyPatches comment documents that emitted PatchEvents carry an empty inversePatches array (hosts keep their own inverse log). - document.ts extractDimensions/extractDuration now use the engine's findRoot — dimension extraction and mutations agree on the root element ([data-hf-root] > #stage > first child). Dimensions prefer the runtime's data-width/data-height forced-override attrs, falling back to inline style. - ownText documented: snapshot .text is trimmed display text; setText writes verbatim. Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush error surfacing, debounce window, path default, history ring-buffer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(lint): add gsap_studio_edit_blocked rule for manual timeline + GSAP element targeting * fix(studio,core): persist manual position edits for GSAP-owned elements - sourceMutation: linkedom CSSStyleDeclaration silently drops CSS custom properties and transform longhands via setProperty; patch the style attribute string directly so --hf-studio-offset-* and translate survive the server round-trip (positions never reached disk before this) - gsapAnimatesTransform(): GSAP owns the full transform stack when it tweens ANY transform prop (scale, rotation, ...), not just x/y — it folds CSS translate into its cache once at init, zeroes the longhand once, and never re-reads it - applyStudioPathOffset: for GSAP-owned elements keep translate:none live and sync the offset into GSAP's cache via gsap.set; writing the longhand double-applied the offset (disappearing elements, scrub snap-back) - buildPathOffsetPatches: emit the var() translate expression explicitly so the persisted file re-folds on reload (live inline is none) - StudioPathOffsetSnapshot: capture/restore GSAP x/y — the drag-response probe mutates GSAP's cache, which inline-style restore cannot undo (click made elements jump by the probe distance) - reapplyPathOffsets: skip GSAP-owned elements (was x/y-only) to stop seek-time double-apply - STUDIO_GSAP_DRAG_INTERCEPT flag (default off): keyframe drag intercept is opt-in until its recording path is hardened; commits take the CSS persist path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(studio): remove duplicate flag declaration, trim useDomEditCommits to 600 lines Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
511665b93a
commit
fc3ab76ce8
@@ -1,3 +1,4 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
export interface StudioManualEditsRenderScriptOptions {
|
||||
activeCompositionPath?: string | null;
|
||||
}
|
||||
|
||||
@@ -223,6 +223,33 @@ function isSafeAttributeValue(name: string, value: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function patchStyleAttrString(style: string, property: string, value: string | null): string {
|
||||
const props = new Map<string, string>();
|
||||
const order: string[] = [];
|
||||
for (const decl of style.split(";")) {
|
||||
const colon = decl.indexOf(":");
|
||||
if (colon < 0) continue;
|
||||
const key = decl.slice(0, colon).trim();
|
||||
const val = decl.slice(colon + 1).trim();
|
||||
if (!key) continue;
|
||||
if (!props.has(key)) order.push(key);
|
||||
props.set(key, val);
|
||||
}
|
||||
if (value === null) {
|
||||
props.delete(property);
|
||||
const idx = order.indexOf(property);
|
||||
if (idx >= 0) order.splice(idx, 1);
|
||||
} else {
|
||||
if (!props.has(property)) order.push(property);
|
||||
props.set(property, value);
|
||||
}
|
||||
return order
|
||||
.map((k) => `${k}: ${props.get(k) ?? ""}`)
|
||||
.filter((d) => d.trim())
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function patchElementInHtml(
|
||||
source: string,
|
||||
target: SourceMutationTarget,
|
||||
@@ -236,10 +263,14 @@ export function patchElementInHtml(
|
||||
for (const op of operations) {
|
||||
switch (op.type) {
|
||||
case "inline-style":
|
||||
if (op.value != null) {
|
||||
htmlEl.style.setProperty(op.property, op.value);
|
||||
} else {
|
||||
htmlEl.style.removeProperty(op.property);
|
||||
// linkedom's CSSStyleDeclaration does not support CSS custom properties
|
||||
// (--foo) or newer individual transform properties (translate, rotate,
|
||||
// scale) via style.setProperty(). Manipulate the style attribute string
|
||||
// directly so all property names survive the round-trip.
|
||||
{
|
||||
const raw = htmlEl.getAttribute("style") ?? "";
|
||||
const patched = patchStyleAttrString(raw, op.property, op.value);
|
||||
htmlEl.setAttribute("style", patched);
|
||||
}
|
||||
break;
|
||||
case "attribute":
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
// GSAP's CSSPlugin takes ownership of the element's entire transform stack
|
||||
// when it tweens ANY of these — it bakes the CSS `translate` longhand into
|
||||
// style.transform at init and writes `translate: none` every tick. Position
|
||||
// reapply/strip logic must therefore stand down for all of them, not just x/y.
|
||||
const GSAP_TRANSFORM_PROPS = [
|
||||
"x",
|
||||
"y",
|
||||
"xPercent",
|
||||
"yPercent",
|
||||
"scale",
|
||||
"scaleX",
|
||||
"scaleY",
|
||||
"rotation",
|
||||
"rotate",
|
||||
"rotationX",
|
||||
"rotationY",
|
||||
"skewX",
|
||||
"skewY",
|
||||
"transform",
|
||||
];
|
||||
|
||||
/**
|
||||
* True when GSAP animates any transform-affecting property on the element,
|
||||
* meaning GSAP owns `style.transform` and has neutralized CSS `translate`.
|
||||
*/
|
||||
export function gsapAnimatesTransform(el: HTMLElement): boolean {
|
||||
return gsapAnimatesProperty(el, ...GSAP_TRANSFORM_PROPS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether GSAP actively animates one or more CSS/GSAP properties on
|
||||
* the given element by inspecting all registered `__timelines`.
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
export function gsapAnimatesProperty(el: HTMLElement, ...props: string[]): boolean {
|
||||
const win = el.ownerDocument.defaultView as
|
||||
| (Window & {
|
||||
|
||||
@@ -47,12 +47,6 @@ export const STUDIO_PREVIEW_MANUAL_EDITING_ENABLED = resolveStudioBooleanEnvFlag
|
||||
true,
|
||||
);
|
||||
|
||||
export const STUDIO_GSAP_DRAG_INTERCEPT_ENABLED = resolveStudioBooleanEnvFlag(
|
||||
env,
|
||||
["VITE_STUDIO_ENABLE_GSAP_DRAG_INTERCEPT"],
|
||||
false,
|
||||
);
|
||||
|
||||
export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag(
|
||||
env,
|
||||
[STUDIO_INSPECTOR_PANELS_ENV, "VITE_STUDIO_INSPECTOR_PANELS_ENABLED"],
|
||||
@@ -89,6 +83,16 @@ export const STUDIO_RAZOR_TOOL_ENABLED = resolveStudioBooleanEnvFlag(
|
||||
false,
|
||||
);
|
||||
|
||||
// When disabled (the default), drag/resize/rotate commits always take the CSS
|
||||
// persist path instead of being intercepted into GSAP script keyframe
|
||||
// mutations. The keyframe intercept rewrites timeline tweens from drag
|
||||
// gestures and is opt-in until its recording path is hardened.
|
||||
export const STUDIO_GSAP_DRAG_INTERCEPT_ENABLED = resolveStudioBooleanEnvFlag(
|
||||
env,
|
||||
["VITE_STUDIO_ENABLE_GSAP_DRAG_INTERCEPT", "VITE_STUDIO_GSAP_DRAG_INTERCEPT_ENABLED"],
|
||||
false,
|
||||
);
|
||||
|
||||
export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
|
||||
|
||||
export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from "./manualEditsTypes";
|
||||
import { roundRotationAngle } from "./manualEditsParsing";
|
||||
import { applyStudioMotionFromDom } from "./studioMotion";
|
||||
import { gsapAnimatesProperty } from "./gsapAnimatesProperty";
|
||||
import { gsapAnimatesProperty, gsapAnimatesTransform } from "./gsapAnimatesProperty";
|
||||
|
||||
/* ── Gesture tracking ─────────────────────────────────────────────── */
|
||||
let studioManualEditGestureId = 0;
|
||||
@@ -223,6 +223,7 @@ function isIdentityAfterTranslateStrip(m: DOMMatrix): boolean {
|
||||
return m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1;
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function stripGsapTranslateFromTransform(element: HTMLElement): void {
|
||||
if (element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR)) return;
|
||||
const transform = element.style.getPropertyValue("transform");
|
||||
@@ -257,6 +258,18 @@ export function applyStudioPathOffset(
|
||||
): void {
|
||||
promoteInlineForTransform(element);
|
||||
writeStudioPathOffsetVars(element, offset, { updateBase: options.updateBase ?? true });
|
||||
if (gsapAnimatesTransform(element)) {
|
||||
// GSAP folded the CSS translate into its transform cache at init and owns
|
||||
// style.transform from then on — it zeroes the translate longhand exactly
|
||||
// once (at fold time) and never re-reads it. Writing translate here would
|
||||
// double-apply the offset on top of the baked transform. Keep translate
|
||||
// neutral in the live DOM and push the offset into GSAP's cache instead;
|
||||
// the var() expression is persisted to the source file by the patch
|
||||
// builder, where a reload re-folds it.
|
||||
element.style.setProperty("translate", "none");
|
||||
syncGsapOwnedTransformPosition(element);
|
||||
return;
|
||||
}
|
||||
element.style.setProperty(
|
||||
"translate",
|
||||
composeTranslateValue(
|
||||
@@ -268,6 +281,24 @@ export function applyStudioPathOffset(
|
||||
stripGsapTranslateFromTransform(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* After committing a new path offset on an element whose transform GSAP owns,
|
||||
* GSAP's internal cache still holds the pre-drag baked translate — the next
|
||||
* seek re-renders from that cache and snaps the element back. Push the new
|
||||
* offset into GSAP so live scrubbing matches what was persisted. (A page
|
||||
* reload re-initializes GSAP from the persisted CSS translate, so this is
|
||||
* only needed for the live session.)
|
||||
*/
|
||||
function syncGsapOwnedTransformPosition(element: HTMLElement): void {
|
||||
if (!gsapAnimatesTransform(element)) return;
|
||||
const win = element.ownerDocument.defaultView as
|
||||
| (Window & { gsap?: { set: (el: Element, vars: Record<string, unknown>) => void } })
|
||||
| null;
|
||||
if (!win?.gsap?.set) return;
|
||||
const { x, y } = readStudioPathOffset(element);
|
||||
win.gsap.set(element, { x, y });
|
||||
}
|
||||
|
||||
export function applyStudioPathOffsetDraft(
|
||||
element: HTMLElement,
|
||||
offset: { x: number; y: number },
|
||||
@@ -275,11 +306,11 @@ export function applyStudioPathOffsetDraft(
|
||||
promoteInlineForTransform(element);
|
||||
writeStudioPathOffsetVars(element, offset, { updateBase: false });
|
||||
|
||||
const isGsapAnimated = gsapAnimatesProperty(element, "x", "y");
|
||||
const isGsapAnimated = gsapAnimatesTransform(element);
|
||||
if (isGsapAnimated) {
|
||||
// For GSAP-animated elements: use gsap.set for positioning (the timeline
|
||||
// is paused during drag). Set translate:none explicitly to prevent
|
||||
// double-counting with the transform.
|
||||
// GSAP owns style.transform (see applyStudioPathOffset): position via
|
||||
// gsap.set while the timeline is paused. Set translate:none explicitly to
|
||||
// prevent double-counting with the baked transform.
|
||||
element.style.setProperty("translate", "none");
|
||||
const win = element.ownerDocument.defaultView as
|
||||
| (Window & { gsap?: { set: (el: Element, vars: Record<string, unknown>) => void } })
|
||||
@@ -520,10 +551,14 @@ function queryStudioElements(doc: Document, attr: string): HTMLElement[] {
|
||||
|
||||
function reapplyPathOffsets(doc: Document): void {
|
||||
for (const el of queryStudioElements(doc, STUDIO_PATH_OFFSET_ATTR)) {
|
||||
const gsapSkip = gsapAnimatesProperty(el, "x", "y");
|
||||
// Skip elements where GSAP owns the transform stack — GSAP bakes the
|
||||
// CSS translate into its transform and sets translate: none every tick
|
||||
// when it tweens ANY transform property (x/y, scale, rotation, ...).
|
||||
// Stripping/restoring would oscillate against GSAP's rendering and
|
||||
// double-apply the offset.
|
||||
if (gsapAnimatesTransform(el)) continue;
|
||||
const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
|
||||
const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
|
||||
if (gsapSkip) continue;
|
||||
if (x || y) {
|
||||
applyStudioPathOffset(
|
||||
el,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
@@ -72,7 +72,23 @@ function appendTransformDisplayOps(element: HTMLElement, ops: PatchOperation[]):
|
||||
|
||||
export function buildPathOffsetPatches(element: HTMLElement): PatchOperation[] {
|
||||
const ops: PatchOperation[] = [];
|
||||
collectInlineStyleOps(element, [STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP, "translate"], ops);
|
||||
collectInlineStyleOps(element, [STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP], ops);
|
||||
// When GSAP owns the element's transform, the live inline translate is kept
|
||||
// at "none" (the offset lives in GSAP's cache — see applyStudioPathOffset).
|
||||
// Persist the var() expression in that case, so a reload re-folds the offset.
|
||||
const inlineTranslate = element.style.getPropertyValue("translate");
|
||||
const hasOffsetVars =
|
||||
element.style.getPropertyValue(STUDIO_OFFSET_X_PROP) ||
|
||||
element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
|
||||
const translateValue =
|
||||
inlineTranslate && inlineTranslate !== "none"
|
||||
? inlineTranslate
|
||||
: hasOffsetVars
|
||||
? `var(${STUDIO_OFFSET_X_PROP}, 0px) var(${STUDIO_OFFSET_Y_PROP}, 0px)`
|
||||
: null;
|
||||
if (translateValue) {
|
||||
ops.push({ type: "inline-style", property: "translate", value: translateValue });
|
||||
}
|
||||
ops.push({ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" });
|
||||
collectAttributeOps(
|
||||
element,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
styleUsesStudioRotation,
|
||||
restoreInlineDisplay,
|
||||
} from "./manualEditsDom";
|
||||
import { gsapAnimatesTransform } from "./gsapAnimatesProperty";
|
||||
import {
|
||||
STUDIO_OFFSET_X_PROP,
|
||||
STUDIO_OFFSET_Y_PROP,
|
||||
@@ -87,7 +88,23 @@ export function captureStudioRotation(element: HTMLElement): StudioRotationSnaps
|
||||
};
|
||||
}
|
||||
|
||||
type GsapWindow = Window & {
|
||||
gsap?: {
|
||||
getProperty?: (el: Element, prop: string) => number | string;
|
||||
set?: (el: Element, vars: Record<string, unknown>) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export function captureStudioPathOffset(element: HTMLElement): StudioPathOffsetSnapshot {
|
||||
let gsapX: number | null = null;
|
||||
let gsapY: number | null = null;
|
||||
if (gsapAnimatesTransform(element)) {
|
||||
const win = element.ownerDocument.defaultView as GsapWindow | null;
|
||||
if (win?.gsap?.getProperty) {
|
||||
gsapX = Number(win.gsap.getProperty(element, "x")) || 0;
|
||||
gsapY = Number(win.gsap.getProperty(element, "y")) || 0;
|
||||
}
|
||||
}
|
||||
return {
|
||||
translate: element.style.getPropertyValue("translate"),
|
||||
x: element.style.getPropertyValue(STUDIO_OFFSET_X_PROP),
|
||||
@@ -95,6 +112,8 @@ export function captureStudioPathOffset(element: HTMLElement): StudioPathOffsetS
|
||||
marker: element.getAttribute(STUDIO_PATH_OFFSET_ATTR),
|
||||
originalTranslate: element.getAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR),
|
||||
originalInlineTranslate: element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR),
|
||||
gsapX,
|
||||
gsapY,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,6 +202,13 @@ export function restoreStudioPathOffset(
|
||||
STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
|
||||
previous.originalInlineTranslate,
|
||||
);
|
||||
|
||||
// Draft positioning on GSAP-owned elements goes through gsap.set, which
|
||||
// mutates GSAP's transform cache — restore it alongside the inline styles.
|
||||
if (previous.gsapX != null || previous.gsapY != null) {
|
||||
const win = element.ownerDocument.defaultView as GsapWindow | null;
|
||||
win?.gsap?.set?.(element, { x: previous.gsapX ?? 0, y: previous.gsapY ?? 0 });
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Clear functions ──────────────────────────────────────────────── */
|
||||
|
||||
@@ -101,4 +101,12 @@ export interface StudioPathOffsetSnapshot {
|
||||
marker: string | null;
|
||||
originalTranslate: string | null;
|
||||
originalInlineTranslate: string | null;
|
||||
/**
|
||||
* GSAP's cached x/y at capture time, for elements whose transform GSAP
|
||||
* owns. Draft positioning mutates GSAP's cache (gsap.set), which inline
|
||||
* style restoration alone cannot undo. Null when GSAP does not own the
|
||||
* element's transform.
|
||||
*/
|
||||
gsapX: number | null;
|
||||
gsapY: number | null;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
||||
import type { useDomEditSession } from "../hooks/useDomEditSession";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { usePlayerStore } from "../player";
|
||||
import { STUDIO_GSAP_DRAG_INTERCEPT_ENABLED } from "../components/editor/manualEditingAvailability";
|
||||
import { FONT_EXT } from "../utils/mediaTypes";
|
||||
import type { PatchOperation } from "../utils/sourcePatcher";
|
||||
import { trackStudioEvent } from "../utils/studioTelemetry";
|
||||
@@ -41,10 +42,13 @@ import type { EditHistoryKind } from "../utils/editHistory";
|
||||
import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
type TimelineLike = { getChildren?: (nested: boolean) => Array<{ targets?: () => Element[] }> };
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function isElementGsapTargeted(iframe: HTMLIFrameElement | null, element: HTMLElement): boolean {
|
||||
// When the GSAP drag intercept is disabled for debugging, treat every
|
||||
// element as un-targeted so commits take the plain CSS persist path.
|
||||
if (!STUDIO_GSAP_DRAG_INTERCEPT_ENABLED) return false;
|
||||
if (!iframe?.contentWindow) return false;
|
||||
let timelines: Record<string, TimelineLike> | undefined;
|
||||
try {
|
||||
@@ -168,6 +172,7 @@ export function useDomEditCommits({
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const persistDomEditOperations: PersistDomEditOperations = useCallback(
|
||||
// fallow-ignore-next-line complexity
|
||||
async (selection, operations, options) => {
|
||||
const pid = projectIdRef.current;
|
||||
if (!pid) throw new Error("No active project");
|
||||
@@ -455,6 +460,7 @@ export function useDomEditCommits({
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const handleDomEditElementDelete = useCallback(
|
||||
// fallow-ignore-next-line complexity
|
||||
async (selection: DomEditSelection) => {
|
||||
const pid = projectIdRef.current;
|
||||
if (!pid) return;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useCallback, useEffect, useRef } from "react";
|
||||
import type { TimelineElement } from "../player";
|
||||
import { usePlayerStore } from "../player";
|
||||
import {
|
||||
STUDIO_GSAP_DRAG_INTERCEPT_ENABLED,
|
||||
STUDIO_GSAP_PANEL_ENABLED,
|
||||
STUDIO_GSAP_DRAG_INTERCEPT_ENABLED,
|
||||
} from "../components/editor/manualEditingAvailability";
|
||||
import { type DomEditSelection } from "../components/editor/domEditing";
|
||||
import { useDomEditPreviewSync } from "./useDomEditPreviewSync";
|
||||
@@ -329,7 +329,7 @@ export function useDomEditSession({
|
||||
// GSAP-aware: intercept offset/resize/rotation to commit via script mutation when animated.
|
||||
const handleGsapAwarePathOffsetCommit = useCallback(
|
||||
async (selection: DomEditSelection, next: { x: number; y: number }) => {
|
||||
if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
|
||||
if (gsapCommitMutation && STUDIO_GSAP_DRAG_INTERCEPT_ENABLED) {
|
||||
const handled = await tryGsapDragIntercept(
|
||||
selection,
|
||||
next,
|
||||
@@ -375,7 +375,7 @@ export function useDomEditSession({
|
||||
|
||||
const handleGsapAwareBoxSizeCommit = useCallback(
|
||||
async (selection: DomEditSelection, next: { width: number; height: number }) => {
|
||||
if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
|
||||
if (gsapCommitMutation && STUDIO_GSAP_DRAG_INTERCEPT_ENABLED) {
|
||||
const handled = await tryGsapResizeIntercept(
|
||||
selection,
|
||||
next,
|
||||
@@ -399,7 +399,7 @@ export function useDomEditSession({
|
||||
|
||||
const handleGsapAwareRotationCommit = useCallback(
|
||||
async (selection: DomEditSelection, next: { angle: number }) => {
|
||||
if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
|
||||
if (gsapCommitMutation && STUDIO_GSAP_DRAG_INTERCEPT_ENABLED) {
|
||||
const handled = await tryGsapRotationIntercept(
|
||||
selection,
|
||||
next.angle,
|
||||
|
||||
Reference in New Issue
Block a user