@hyperframes/studio 0.8.93 → 0.8.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-CkV9KK6A.js → hyperframes-player-DJJKab4_.js} +1 -1
- package/dist/assets/{index-V3fzEfOD.js → index-C1dXTZvs.js} +1 -1
- package/dist/assets/{index-CYk0NwSv.js → index-Cc4by8r7.js} +183 -183
- package/dist/assets/{index-BlLFipOk.js → index-DE1dn3QB.js} +1 -1
- package/dist/index.d.ts +63 -46
- package/dist/index.html +1 -1
- package/dist/index.js +196 -111
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/components/TimelineToolbar.test.tsx +17 -0
- package/src/components/TimelineToolbar.tsx +3 -1
- package/src/components/editor/ConnectedDomEditOverlay.test.tsx +2 -0
- package/src/components/editor/ConnectedDomEditOverlay.tsx +3 -1
- package/src/components/editor/DomEditCropHandles.test.tsx +87 -1
- package/src/components/editor/DomEditCropHandles.tsx +64 -62
- package/src/components/editor/DomEditOverlay.test.ts +39 -0
- package/src/components/editor/DomEditOverlay.tsx +6 -1
- package/src/components/editor/DomEditSelectionChrome.test.tsx +114 -1
- package/src/components/editor/DomEditSelectionChrome.tsx +17 -10
- package/src/components/editor/PreviewOverlayProvider.test.tsx +19 -0
- package/src/components/editor/PreviewOverlayProvider.tsx +1 -1
- package/src/components/editor/SnapToolbar.tsx +1 -1
- package/src/components/editor/domEditOverlayGeometry.ts +2 -0
- package/src/components/editor/manualEditsSeekReapply.ts +5 -1
- package/src/components/nle/PreviewOverlays.tsx +3 -3
- package/src/hooks/gsapResizeIntercept.test.ts +40 -0
- package/src/hooks/gsapResizeIntercept.ts +8 -4
- package/src/index.ts +4 -0
- package/src/player/components/PlayerControls.tsx +2 -15
- package/src/player/components/ShortcutsPanel.test.tsx +33 -1
- package/src/player/components/ShortcutsPanel.tsx +26 -0
- package/src/player/components/Timeline.ghostLane.test.tsx +126 -0
- package/src/player/components/Timeline.rulerSeek.test.tsx +7 -0
- package/src/player/components/Timeline.test.ts +3 -3
- package/src/player/components/Timeline.trackPadding.test.tsx +157 -0
- package/src/player/components/TimelineCanvas.tsx +44 -19
- package/src/player/components/TimelineProvider.tsx +1 -0
- package/src/player/components/TimelineTypes.ts +2 -0
- package/src/player/components/timelineDragDrop.test.tsx +14 -12
- package/src/player/components/timelineLayout.test.ts +87 -11
- package/src/player/components/timelineLayout.ts +28 -21
- package/src/player/components/useTimelineProviderState.tsx +5 -1
- package/src/player/components/useTimelineTrackLayout.ts +11 -2
- package/src/player/components/useTimelineVirtualRows.test.tsx +32 -0
- package/src/player/components/useTimelineVirtualRows.ts +2 -2
- package/src/utils/gsapSoftReload.test.ts +51 -2
- package/src/utils/gsapSoftReload.ts +24 -10
- package/src/utils/gsapUndoRestore.test.ts +49 -0
- package/src/utils/gsapUndoRestore.ts +15 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
TRACK_H,
|
|
11
11
|
createTimelineRowGeometry,
|
|
12
12
|
type TimelineRowGeometry,
|
|
13
|
+
type TimelineTrackPadding,
|
|
13
14
|
trackHeights,
|
|
14
15
|
type TimelineTrackHeightClip,
|
|
15
16
|
} from "./timelineLayout";
|
|
@@ -164,7 +165,10 @@ function useTimelineRowHeights(
|
|
|
164
165
|
selectedElementId: string | null,
|
|
165
166
|
selectedElementIds: ReadonlySet<string>,
|
|
166
167
|
groups: readonly TimelineTrackGroupInfo[],
|
|
168
|
+
trackPadding: TimelineTrackPadding | undefined,
|
|
167
169
|
) {
|
|
170
|
+
const padTop = trackPadding?.top;
|
|
171
|
+
const padBottom = trackPadding?.bottom;
|
|
168
172
|
const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
|
|
169
173
|
const expandedLaneOwnerIds = usePlayerStore((s) => s.expandedLaneOwnerIds);
|
|
170
174
|
const { laneCounts, rowGeometry } = useMemo(() => {
|
|
@@ -207,9 +211,12 @@ function useTimelineRowHeights(
|
|
|
207
211
|
rowGeometry: createTimelineRowGeometry(
|
|
208
212
|
tracks.map(([track]) => track),
|
|
209
213
|
rowHeights,
|
|
214
|
+
{ top: padTop, bottom: padBottom },
|
|
210
215
|
),
|
|
211
216
|
};
|
|
212
217
|
}, [
|
|
218
|
+
padTop,
|
|
219
|
+
padBottom,
|
|
213
220
|
expandedClipIds,
|
|
214
221
|
expandedLaneOwnerIds,
|
|
215
222
|
gsapAnimations,
|
|
@@ -233,6 +240,7 @@ export function useTimelineTrackLayout(
|
|
|
233
240
|
gsapAnimations: Map<string, GsapAnimation[]>,
|
|
234
241
|
selectedElementId: string | null,
|
|
235
242
|
selectedElementIds: ReadonlySet<string>,
|
|
243
|
+
trackPadding?: TimelineTrackPadding,
|
|
236
244
|
) {
|
|
237
245
|
const { tracks, trackStyles, trackOrder, groups, trackGroupOf } =
|
|
238
246
|
useTimelineTrackDerivations(expandedElements);
|
|
@@ -244,6 +252,7 @@ export function useTimelineTrackLayout(
|
|
|
244
252
|
selectedElementId,
|
|
245
253
|
selectedElementIds,
|
|
246
254
|
groups,
|
|
255
|
+
trackPadding,
|
|
247
256
|
);
|
|
248
257
|
|
|
249
258
|
return {
|
|
@@ -305,8 +314,8 @@ export function useTimelineDisplayLayout(
|
|
|
305
314
|
const displayTrackOrder = useDisplayTrackOrder(draggedClip, trackOrder);
|
|
306
315
|
const displayRowHeights = useDisplayRowHeights(displayTrackOrder, rowGeometry);
|
|
307
316
|
const displayRowGeometry = useMemo(
|
|
308
|
-
() => createTimelineRowGeometry(displayTrackOrder, displayRowHeights),
|
|
309
|
-
[displayTrackOrder, displayRowHeights],
|
|
317
|
+
() => createTimelineRowGeometry(displayTrackOrder, displayRowHeights, rowGeometry.padding),
|
|
318
|
+
[displayTrackOrder, displayRowHeights, rowGeometry.padding],
|
|
310
319
|
);
|
|
311
320
|
return {
|
|
312
321
|
displayTrackOrder,
|
|
@@ -171,4 +171,36 @@ describe("useTimelineVirtualRows", () => {
|
|
|
171
171
|
|
|
172
172
|
act(() => root.unmount());
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
it("offsets the mounted range by the geometry's own top pad", () => {
|
|
176
|
+
const keys = Array.from({ length: 1_000 }, (_, index) => index);
|
|
177
|
+
const topPad = 200 * 48;
|
|
178
|
+
const geometry = createTimelineRowGeometry(
|
|
179
|
+
keys,
|
|
180
|
+
keys.map(() => 48),
|
|
181
|
+
{ top: topPad },
|
|
182
|
+
);
|
|
183
|
+
const scroll = createScrollElement(RULER_H + topPad + 300 * 48);
|
|
184
|
+
const scrollRef = { current: scroll };
|
|
185
|
+
let rows: ReturnType<typeof useTimelineVirtualRows> = [];
|
|
186
|
+
|
|
187
|
+
function Probe() {
|
|
188
|
+
rows = useTimelineVirtualRows({
|
|
189
|
+
enabled: true,
|
|
190
|
+
scrollRef,
|
|
191
|
+
viewport: viewport(scroll),
|
|
192
|
+
rowGeometry: geometry,
|
|
193
|
+
sessionEpoch: 1,
|
|
194
|
+
pinnedRowKeys: [],
|
|
195
|
+
});
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const root = createRoot(document.createElement("div"));
|
|
200
|
+
act(() => root.render(React.createElement(Probe)));
|
|
201
|
+
act(() => {});
|
|
202
|
+
expect(rows.some((row) => row.index === 300)).toBe(true);
|
|
203
|
+
expect(rows.some((row) => row.index === 500)).toBe(false);
|
|
204
|
+
act(() => root.unmount());
|
|
205
|
+
});
|
|
174
206
|
});
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, type RefObject } from "react";
|
|
|
2
2
|
import { defaultRangeExtractor, useVirtualizer, type Range } from "@tanstack/react-virtual";
|
|
3
3
|
import { TIMELINE_VIEWPORT_BUDGETS } from "../lib/timelineViewportBudgets";
|
|
4
4
|
import type { TimelineScrollViewportSnapshot } from "./useTimelineScrollViewport";
|
|
5
|
-
import { RULER_H,
|
|
5
|
+
import { RULER_H, type TimelineRowGeometry } from "./timelineLayout";
|
|
6
6
|
|
|
7
7
|
export interface TimelineVirtualRow {
|
|
8
8
|
readonly index: number;
|
|
@@ -84,7 +84,7 @@ export function useTimelineVirtualRows({
|
|
|
84
84
|
getItemKey,
|
|
85
85
|
overscan: TIMELINE_VIEWPORT_BUDGETS.rowOverscanPerSide,
|
|
86
86
|
rangeExtractor,
|
|
87
|
-
scrollMargin: RULER_H +
|
|
87
|
+
scrollMargin: RULER_H + rowGeometry.padding.top,
|
|
88
88
|
initialRect: { width: viewport.clientWidth, height: viewport.clientHeight },
|
|
89
89
|
initialOffset,
|
|
90
90
|
});
|
|
@@ -426,8 +426,8 @@ describe("ensureMotionPathPluginLoaded", () => {
|
|
|
426
426
|
// to its AUTHORED value — from the after-write file HTML when provided, else
|
|
427
427
|
// from the parse-time stamp. Otherwise a runtime transient (the color-grading
|
|
428
428
|
// hide's 0, a mid-flight tween value) becomes a permanent tween bound.
|
|
429
|
-
describe("applySoftReload authored-
|
|
430
|
-
function buildIframeWithTarget(el:
|
|
429
|
+
describe("applySoftReload authored-style restore", () => {
|
|
430
|
+
function buildIframeWithTarget(el: Element, overrides: Record<string, unknown> = {}) {
|
|
431
431
|
const scriptEl = document.createElement("script");
|
|
432
432
|
scriptEl.textContent =
|
|
433
433
|
'const tl = gsap.timeline({ paused: true }); tl.to("#box", { opacity: 0.5 });';
|
|
@@ -532,4 +532,53 @@ describe("applySoftReload authored-opacity restore", () => {
|
|
|
532
532
|
|
|
533
533
|
expect(restoreOpacity(el)).toBe("");
|
|
534
534
|
});
|
|
535
|
+
|
|
536
|
+
it("restores the file's inline translate, rotate and scale and drops GSAP's transform", () => {
|
|
537
|
+
const el = document.createElement("div");
|
|
538
|
+
el.setAttribute("data-hf-id", "hf-1");
|
|
539
|
+
el.style.cssText =
|
|
540
|
+
"left: 10px; translate: none; rotate: none; scale: none; transform: translate(9px, 9px)";
|
|
541
|
+
const { iframe } = buildIframeWithTarget(el);
|
|
542
|
+
const authoredHtml = `<html><body><div data-hf-id="hf-1" style="translate: 60px 40px; rotate: 15deg; scale: 1.5"></div></body></html>`;
|
|
543
|
+
expect(applySoftReload(iframe, SCRIPT_TEXT, { authoredHtml })).toBe("applied");
|
|
544
|
+
const read = (prop: string) => el.style.getPropertyValue(prop);
|
|
545
|
+
expect(["translate", "rotate", "scale", "transform"].map(read)).toEqual([
|
|
546
|
+
"60px 40px",
|
|
547
|
+
"15deg",
|
|
548
|
+
"1.5",
|
|
549
|
+
"",
|
|
550
|
+
]);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("keeps an SVG child's authored inline transform", () => {
|
|
554
|
+
const el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
555
|
+
el.setAttribute("data-hf-id", "hf-1");
|
|
556
|
+
el.setAttribute("style", "transform: translate(9px, 9px) rotate(20deg)");
|
|
557
|
+
const { iframe } = buildIframeWithTarget(el);
|
|
558
|
+
const authoredHtml = `<html><body><svg><rect data-hf-id="hf-1" style="transform: rotate(20deg)"></rect></svg></body></html>`;
|
|
559
|
+
applySoftReload(iframe, SCRIPT_TEXT, { authoredHtml });
|
|
560
|
+
expect(el.style.transform).toBe("rotate(20deg)");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("the finalize seek cannot paint the killed timeline over the restored transform", () => {
|
|
564
|
+
const el = document.createElement("div");
|
|
565
|
+
el.style.cssText = "translate: none; transform: translate3d(77.5px, 40px, 0px)";
|
|
566
|
+
const children = [{ targets: () => [el] }];
|
|
567
|
+
const killed = {
|
|
568
|
+
kill: vi.fn(),
|
|
569
|
+
getChildren: () => children,
|
|
570
|
+
clear: () => void children.splice(0),
|
|
571
|
+
};
|
|
572
|
+
// The runtime still seeks the timeline it captured at load until the rebind swaps it.
|
|
573
|
+
const seek = () =>
|
|
574
|
+
children.forEach(() => (el.style.transform = "translate3d(77.5px, 0px, 0px)"));
|
|
575
|
+
let transformAtRebind: string | null = null;
|
|
576
|
+
const { iframe } = buildIframeWithTarget(el, {
|
|
577
|
+
__timelines: { root: killed },
|
|
578
|
+
__player: { getTime: () => 1, seek },
|
|
579
|
+
__hfForceTimelineRebind: () => (transformAtRebind = el.style.transform),
|
|
580
|
+
});
|
|
581
|
+
applySoftReload(iframe, SCRIPT_TEXT);
|
|
582
|
+
expect(transformAtRebind).toBe("");
|
|
583
|
+
});
|
|
535
584
|
});
|
|
@@ -131,6 +131,18 @@ function verifyTimelinesPopulated(win: IframeWindow, targetKeys: string[]): bool
|
|
|
131
131
|
return Object.keys(timelines).filter((k) => k !== "__proxied").length > 0;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// GSAP masks a folded CSS translate/rotate/scale with `none`; a fresh load has only what the file authors.
|
|
135
|
+
function restoreAuthoredTransforms(
|
|
136
|
+
style: CSSStyleDeclaration,
|
|
137
|
+
authored: CSSStyleDeclaration | null,
|
|
138
|
+
) {
|
|
139
|
+
style.transform = authored?.transform ?? "";
|
|
140
|
+
if (!authored) return;
|
|
141
|
+
for (const prop of ["translate", "rotate", "scale"]) {
|
|
142
|
+
style.setProperty(prop, authored.getPropertyValue(prop));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
/**
|
|
135
147
|
* Outcome of a soft-reload attempt. Callers must distinguish PERMANENT failures
|
|
136
148
|
* (the preview genuinely can't be soft-updated — escalate to a full reload) from
|
|
@@ -181,7 +193,7 @@ export interface SoftReloadOptions {
|
|
|
181
193
|
onAsyncFailure?: () => void;
|
|
182
194
|
/** Seek target for the rebuilt timeline; defaults to the iframe player time. */
|
|
183
195
|
currentTimeOverride?: number;
|
|
184
|
-
/** After-write file HTML — the primary source for authored
|
|
196
|
+
/** After-write file HTML — the primary source for the authored opacity and transform restore. */
|
|
185
197
|
authoredHtml?: string;
|
|
186
198
|
}
|
|
187
199
|
|
|
@@ -299,7 +311,7 @@ export function applySoftReload(
|
|
|
299
311
|
// parse-time stamp (data-hf-authored-opacity, installAuthoredOpacityCapture)
|
|
300
312
|
// covers elements the file lookup can't resolve. Parsed lazily, at most once.
|
|
301
313
|
let authoredDoc: Document | null | undefined;
|
|
302
|
-
const
|
|
314
|
+
const findAuthoredStyle = (el: HTMLElement): CSSStyleDeclaration | null => {
|
|
303
315
|
if (authoredDoc === undefined) {
|
|
304
316
|
try {
|
|
305
317
|
authoredDoc = authoredHtml ? parseSavedSource(authoredHtml) : null;
|
|
@@ -307,13 +319,12 @@ export function applySoftReload(
|
|
|
307
319
|
authoredDoc = null;
|
|
308
320
|
}
|
|
309
321
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const source = findAuthoredSource(el);
|
|
314
|
-
if (source instanceof HTMLElement) return source.style.opacity;
|
|
315
|
-
return readStampedAuthoredOpacity(el);
|
|
322
|
+
const source = authoredDoc ? findAuthoredElement(authoredDoc, el) : null;
|
|
323
|
+
// The parsed file lives in this realm, so instanceof holds here, unlike for the iframe nodes below.
|
|
324
|
+
return source instanceof HTMLElement || source instanceof SVGElement ? source.style : null;
|
|
316
325
|
};
|
|
326
|
+
const readAuthoredOpacity = (el: HTMLElement): string | null =>
|
|
327
|
+
findAuthoredStyle(el)?.opacity ?? readStampedAuthoredOpacity(el);
|
|
317
328
|
|
|
318
329
|
// fallow-ignore-next-line complexity
|
|
319
330
|
const doReload = () => {
|
|
@@ -327,6 +338,7 @@ export function applySoftReload(
|
|
|
327
338
|
const tl = timelines[key] as
|
|
328
339
|
| {
|
|
329
340
|
kill?: () => void;
|
|
341
|
+
clear?: () => void;
|
|
330
342
|
getChildren?: (deep: boolean) => Array<{ targets?: () => Element[] }>;
|
|
331
343
|
}
|
|
332
344
|
| undefined;
|
|
@@ -341,6 +353,8 @@ export function applySoftReload(
|
|
|
341
353
|
} catch {}
|
|
342
354
|
}
|
|
343
355
|
try {
|
|
356
|
+
// kill() keeps the children, and the finalize seek renders this timeline until the rebind swaps it.
|
|
357
|
+
tl.clear?.();
|
|
344
358
|
tl.kill?.();
|
|
345
359
|
} catch {}
|
|
346
360
|
delete timelines[key];
|
|
@@ -369,7 +383,7 @@ export function applySoftReload(
|
|
|
369
383
|
// Reset GSAP's internal transform cache so from() tweens don't read stale
|
|
370
384
|
// end values. `clearProps: "all"` is needed to flush the cache, but it also
|
|
371
385
|
// nukes the element's CSS base (position, width, height, etc.) from the
|
|
372
|
-
// HTML `style=""` attribute. Save → clear → restore →
|
|
386
|
+
// HTML `style=""` attribute. Save → clear → restore → authored transform props.
|
|
373
387
|
if (allTargets.length > 0 && win.gsap?.set) {
|
|
374
388
|
const saved: Array<[HTMLElement, string]> = [];
|
|
375
389
|
for (const el of allTargets) {
|
|
@@ -384,7 +398,7 @@ export function applySoftReload(
|
|
|
384
398
|
for (const [el, css] of saved) {
|
|
385
399
|
const s = el.style;
|
|
386
400
|
s.cssText = css;
|
|
387
|
-
s
|
|
401
|
+
restoreAuthoredTransforms(s, findAuthoredStyle(el));
|
|
388
402
|
// The restored cssText carries RUNTIME opacity, not authored opacity:
|
|
389
403
|
// a mid-flight tween's interpolated value, or the color-grading hide
|
|
390
404
|
// (`opacity: 0 !important`). The re-run script's tweens re-initialize
|
|
@@ -161,6 +161,55 @@ describe("applyUndoRestoreToPreview", () => {
|
|
|
161
161
|
},
|
|
162
162
|
);
|
|
163
163
|
|
|
164
|
+
describe("an undo that re-runs a changed script matches a fresh load of the restored file", () => {
|
|
165
|
+
const script = (extra: string) => `window.__timelines["root"]=gsap.timeline();${extra}`;
|
|
166
|
+
const undoScriptEdit = (live: string, authored: string, edit: string) => {
|
|
167
|
+
const { iframe, contentWindow, doc } = buildLiveIframe(
|
|
168
|
+
`${live}<script>${script(edit)}</script>`,
|
|
169
|
+
);
|
|
170
|
+
const clearProps = (targets: HTMLElement[]) =>
|
|
171
|
+
targets.forEach((t) => t.removeAttribute("style"));
|
|
172
|
+
Object.assign(contentWindow.gsap, { set: clearProps });
|
|
173
|
+
for (const el of doc.querySelectorAll("[style*=transform]")) Object.assign(el, { _gsap: {} });
|
|
174
|
+
const files = {
|
|
175
|
+
[ROOT]: {
|
|
176
|
+
previous: wrap(`${authored}<script>${script(edit)}</script>`),
|
|
177
|
+
restored: wrap(`${authored}<script>${script("")}</script>`),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
expect(applyUndoRestoreToPreview(iframe, ROOT, files, 3, vi.fn())).toBe("soft");
|
|
181
|
+
return doc;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
it("drops the size marks a resize left on the live element", () => {
|
|
185
|
+
const doc = undoScriptEdit(
|
|
186
|
+
`<div id="a" data-hf-studio-box-size="true" style="width: 502px; --hf-studio-width: 502px; --hf-studio-height: 334px">t</div>`,
|
|
187
|
+
`<div id="a" style="width: 300px">t</div>`,
|
|
188
|
+
`tl.set("#a",{width:502,height:334},0);`,
|
|
189
|
+
);
|
|
190
|
+
expect(doc.getElementById("a")!.hasAttribute("data-hf-studio-box-size")).toBe(false);
|
|
191
|
+
expect(doc.getElementById("a")!.getAttribute("style")).toBe("width: 300px");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("keeps the authored inline rotation of an element GSAP moved", () => {
|
|
195
|
+
const doc = undoScriptEdit(
|
|
196
|
+
`<div id="r" style="left: 10px; transform: translate(116px, 72px) rotate(20deg)">t</div>`,
|
|
197
|
+
`<div id="r" style="left: 10px; transform: rotate(20deg)">t</div>`,
|
|
198
|
+
`gsap.set("#r",{x:116,y:72});`,
|
|
199
|
+
);
|
|
200
|
+
expect(doc.getElementById("r")!.style.transform).toBe("rotate(20deg)");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("drops the translate mask GSAP wrote over a stylesheet translate", () => {
|
|
204
|
+
const doc = undoScriptEdit(
|
|
205
|
+
`<h1 id="t" style="opacity: 0.5; translate: none; transform: translate(116px, -113px)">t</h1>`,
|
|
206
|
+
`<h1 id="t">t</h1>`,
|
|
207
|
+
`gsap.set("#t",{x:116,y:-113});`,
|
|
208
|
+
);
|
|
209
|
+
expect(doc.getElementById("t")!.style.getPropertyValue("translate")).toBe("");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
164
213
|
it("full-reloads without partially restoring when any changed live target is missing", () => {
|
|
165
214
|
const { iframe, doc } = buildLiveIframe(`<div id="a" style="z-index: 8">a</div>`);
|
|
166
215
|
const reloadPreview = vi.fn();
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
extractGsapScriptText,
|
|
9
9
|
findGsapScriptElements,
|
|
10
10
|
} from "./gsapSoftReload";
|
|
11
|
+
import { findAuthoredElement, parseSavedSource } from "./authoredSource";
|
|
12
|
+
import { STUDIO_EDIT_ATTRS } from "../components/editor/manualEditsSeekReapply";
|
|
11
13
|
import { markScenesStale } from "../player/sceneSwap";
|
|
12
14
|
|
|
13
15
|
type PreviewWindow = Window & {
|
|
@@ -135,6 +137,17 @@ function syncElementAttributes(target: Element, source: Element): void {
|
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
// A gesture folded into the script leaves marks on the live element that every seek would re-impose.
|
|
141
|
+
function syncStaleEditMarks(doc: Document, restored: string): void {
|
|
142
|
+
const restoredDoc = parseSavedSource(restored);
|
|
143
|
+
for (const live of doc.querySelectorAll(STUDIO_EDIT_ATTRS.map((attr) => `[${attr}]`).join())) {
|
|
144
|
+
const source = findAuthoredElement(restoredDoc, live);
|
|
145
|
+
if (source && STUDIO_EDIT_ATTRS.some((a) => live.getAttribute(a) !== source.getAttribute(a))) {
|
|
146
|
+
syncElementAttributes(live, source);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
function readGsapScriptTexts(html: string): string[] {
|
|
139
152
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
140
153
|
return findGsapScriptElements(doc).map((script) => script.textContent ?? "");
|
|
@@ -248,9 +261,11 @@ export function applyUndoRestoreToPreview(
|
|
|
248
261
|
const restoredScript = extractGsapScriptText(restored);
|
|
249
262
|
const previousScript = extractGsapScriptText(previous);
|
|
250
263
|
if (restoredScript && restoredScript !== previousScript) {
|
|
264
|
+
syncStaleEditMarks(doc, restored);
|
|
251
265
|
const result = applySoftReload(iframe, restoredScript, {
|
|
252
266
|
onAsyncFailure: reloadPreview,
|
|
253
267
|
currentTimeOverride: currentTime,
|
|
268
|
+
authoredHtml: restored,
|
|
254
269
|
});
|
|
255
270
|
if (result === "cannot-soft-reload") {
|
|
256
271
|
reloadPreview();
|