@hyperframes/studio 0.7.57 → 0.7.59
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--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +27 -7
- package/dist/index.html +2 -2
- package/dist/index.js +18029 -11563
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +6 -6
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +55 -1
- package/src/hooks/timelineTrackVisibility.ts +31 -13
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +81 -162
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +182 -84
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +306 -155
- package/src/hooks/useTimelineEditing.ts +111 -101
- package/src/hooks/useTimelineEditingTypes.ts +4 -1
- package/src/hooks/useTimelineGroupEditing.ts +51 -5
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-DfmYkU44.js +0 -423
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
import { describe, it, expect, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
applySoftReload,
|
|
6
|
+
applySoftReloadFinalization,
|
|
6
7
|
ensureMotionPathPluginLoaded,
|
|
7
|
-
diffSoftReloadableRestore,
|
|
8
|
-
applyUndoRestoreToPreview,
|
|
9
8
|
} from "./gsapSoftReload";
|
|
10
9
|
|
|
11
10
|
const SCRIPT_TEXT = `
|
|
@@ -66,6 +65,7 @@ function buildMockIframe(overrides: Record<string, unknown> = {}) {
|
|
|
66
65
|
iframe: { contentWindow, contentDocument } as unknown as HTMLIFrameElement,
|
|
67
66
|
contentWindow,
|
|
68
67
|
mockTimeline,
|
|
68
|
+
container,
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -288,6 +288,58 @@ describe("applySoftReload", () => {
|
|
|
288
288
|
});
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
+
// ── Finalization-only path: seek → rebind → manual edits, with NO script
|
|
292
|
+
// execution — the flashless sync for timing edits that changed no script.
|
|
293
|
+
describe("applySoftReloadFinalization", () => {
|
|
294
|
+
it("seeks, rebinds, and reapplies manual edits without touching any script", () => {
|
|
295
|
+
const { iframe, contentWindow, container, mockTimeline } = buildMockIframe();
|
|
296
|
+
const scriptsBefore = container.querySelectorAll("script").length;
|
|
297
|
+
|
|
298
|
+
expect(applySoftReloadFinalization(iframe, 2.0)).toBe(true);
|
|
299
|
+
|
|
300
|
+
expect(contentWindow.__player.seek).toHaveBeenCalledWith(2.0);
|
|
301
|
+
expect(contentWindow.__hfForceTimelineRebind).toHaveBeenCalledTimes(1);
|
|
302
|
+
expect(contentWindow.__hfStudioManualEditsApply).toHaveBeenCalledTimes(1);
|
|
303
|
+
// No script executed or removed; the live timeline was never killed.
|
|
304
|
+
expect(container.querySelectorAll("script").length).toBe(scriptsBefore);
|
|
305
|
+
expect(mockTimeline.kill).not.toHaveBeenCalled();
|
|
306
|
+
expect(contentWindow.__timelines.root).toBe(mockTimeline);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("runs inside __hfSuppressSceneMutations when the runtime provides it", () => {
|
|
310
|
+
const suppress = vi.fn(<T>(fn: () => T): T => fn());
|
|
311
|
+
const { iframe, contentWindow } = buildMockIframe({
|
|
312
|
+
__hfSuppressSceneMutations: suppress,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(applySoftReloadFinalization(iframe, 1.5)).toBe(true);
|
|
316
|
+
|
|
317
|
+
expect(suppress).toHaveBeenCalledTimes(1);
|
|
318
|
+
expect(contentWindow.__hfForceTimelineRebind).toHaveBeenCalledTimes(1);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("does NOT require gsap — a script-less runtime with the rebind hook works", () => {
|
|
322
|
+
const { iframe, contentWindow } = buildMockIframe({ gsap: undefined });
|
|
323
|
+
expect(applySoftReloadFinalization(iframe, 0)).toBe(true);
|
|
324
|
+
expect(contentWindow.__hfForceTimelineRebind).toHaveBeenCalledTimes(1);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("returns false when the iframe or the rebind hook is unavailable", () => {
|
|
328
|
+
expect(applySoftReloadFinalization(null, 0)).toBe(false);
|
|
329
|
+
const { iframe } = buildMockIframe({ __hfForceTimelineRebind: undefined });
|
|
330
|
+
expect(applySoftReloadFinalization(iframe, 0)).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("returns false when the rebind throws (caller full-reloads)", () => {
|
|
334
|
+
const { iframe } = buildMockIframe({
|
|
335
|
+
__hfForceTimelineRebind: vi.fn(() => {
|
|
336
|
+
throw new Error("runtime mid-teardown");
|
|
337
|
+
}),
|
|
338
|
+
});
|
|
339
|
+
expect(applySoftReloadFinalization(iframe, 0)).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
291
343
|
function buildBootstrapIframe(overrides: Record<string, unknown> = {}) {
|
|
292
344
|
const head = document.createElement("div");
|
|
293
345
|
const appendedScripts: HTMLScriptElement[] = [];
|
|
@@ -452,116 +504,3 @@ describe("applySoftReload authored-opacity restore", () => {
|
|
|
452
504
|
expect(restoreOpacity(el)).toBe("");
|
|
453
505
|
});
|
|
454
506
|
});
|
|
455
|
-
|
|
456
|
-
// ── Bug 2: undo/redo restore soft-apply ──────────────────────────────────────
|
|
457
|
-
|
|
458
|
-
const wrap = (body: string) => `<html><body>${body}</body></html>`;
|
|
459
|
-
|
|
460
|
-
describe("diffSoftReloadableRestore", () => {
|
|
461
|
-
it("reports the changed id for an attribute/inline-style-only diff", () => {
|
|
462
|
-
const prev = wrap(`<div id="a" style="translate: 10px 10px">t</div>`);
|
|
463
|
-
const next = wrap(`<div id="a" style="translate: 0px 0px">t</div>`);
|
|
464
|
-
expect(diffSoftReloadableRestore(prev, next)).toEqual({ changedElementIds: ["a"] });
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it("treats a structural change (added element) as NOT soft-reloadable", () => {
|
|
468
|
-
const prev = wrap(`<div id="a">t</div>`);
|
|
469
|
-
const next = wrap(`<div id="a">t</div><div id="a-split">t</div>`);
|
|
470
|
-
expect(diffSoftReloadableRestore(prev, next)).toBeNull();
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it("treats an element text/child change as NOT soft-reloadable", () => {
|
|
474
|
-
const prev = wrap(`<div id="a">one</div>`);
|
|
475
|
-
const next = wrap(`<div id="a">two</div>`);
|
|
476
|
-
expect(diffSoftReloadableRestore(prev, next)).toBeNull();
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
it("allows a GSAP-script-only change (no id'd-attribute diff)", () => {
|
|
480
|
-
const prev = wrap(
|
|
481
|
-
`<div id="a">t</div><script>window.__timelines["root"]=gsap.timeline().to("#a",{x:1});</script>`,
|
|
482
|
-
);
|
|
483
|
-
const next = wrap(
|
|
484
|
-
`<div id="a">t</div><script>window.__timelines["root"]=gsap.timeline().to("#a",{x:9});</script>`,
|
|
485
|
-
);
|
|
486
|
-
expect(diffSoftReloadableRestore(prev, next)).toEqual({ changedElementIds: [] });
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
function buildLiveIframe(bodyHtml: string) {
|
|
491
|
-
const doc = document.implementation.createHTMLDocument("");
|
|
492
|
-
doc.body.innerHTML = bodyHtml;
|
|
493
|
-
const contentWindow = {
|
|
494
|
-
gsap: { timeline: () => {} },
|
|
495
|
-
__hfForceTimelineRebind: () => {},
|
|
496
|
-
__timelines: {} as Record<string, unknown>,
|
|
497
|
-
__player: { getTime: () => 3, seek: vi.fn() },
|
|
498
|
-
__hfStudioManualEditsApply: vi.fn(),
|
|
499
|
-
};
|
|
500
|
-
return {
|
|
501
|
-
iframe: { contentWindow, contentDocument: doc } as unknown as HTMLIFrameElement,
|
|
502
|
-
contentWindow,
|
|
503
|
-
doc,
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
describe("applyUndoRestoreToPreview", () => {
|
|
508
|
-
const ROOT = "index.html";
|
|
509
|
-
|
|
510
|
-
it("soft-applies an attribute/style-only restore: syncs the live element, no full reload", () => {
|
|
511
|
-
const { iframe, contentWindow, doc } = buildLiveIframe(
|
|
512
|
-
`<div id="a" style="translate: 10px 10px" data-hf-path-offset="true">t</div>`,
|
|
513
|
-
);
|
|
514
|
-
const reloadPreview = vi.fn();
|
|
515
|
-
const files = {
|
|
516
|
-
[ROOT]: {
|
|
517
|
-
previous: wrap(
|
|
518
|
-
`<div id="a" style="translate: 10px 10px" data-hf-path-offset="true">t</div>`,
|
|
519
|
-
),
|
|
520
|
-
restored: wrap(`<div id="a" style="translate: 0px 0px" data-hf-path-offset="true">t</div>`),
|
|
521
|
-
},
|
|
522
|
-
};
|
|
523
|
-
const outcome = applyUndoRestoreToPreview(iframe, ROOT, files, 3, reloadPreview);
|
|
524
|
-
expect(outcome).toBe("soft");
|
|
525
|
-
expect(reloadPreview).not.toHaveBeenCalled();
|
|
526
|
-
// Live element reverted to the restored inline style.
|
|
527
|
-
expect(doc.getElementById("a")!.getAttribute("style")).toBe("translate: 0px 0px");
|
|
528
|
-
// No GSAP script in the restore → the manual-edit reapply runs, playhead held.
|
|
529
|
-
expect(contentWindow.__player.seek).toHaveBeenCalledWith(3);
|
|
530
|
-
expect(contentWindow.__hfStudioManualEditsApply).toHaveBeenCalled();
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it("full-reloads a multi-file restore", () => {
|
|
534
|
-
const { iframe } = buildLiveIframe(`<div id="a">t</div>`);
|
|
535
|
-
const reloadPreview = vi.fn();
|
|
536
|
-
const files = {
|
|
537
|
-
[ROOT]: {
|
|
538
|
-
previous: wrap(`<div id="a" style="x">t</div>`),
|
|
539
|
-
restored: wrap(`<div id="a">t</div>`),
|
|
540
|
-
},
|
|
541
|
-
"scenes/intro.html": { previous: "a", restored: "b" },
|
|
542
|
-
};
|
|
543
|
-
expect(applyUndoRestoreToPreview(iframe, ROOT, files, 3, reloadPreview)).toBe("full");
|
|
544
|
-
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it("full-reloads a structural restore (split/delete undo)", () => {
|
|
548
|
-
const { iframe } = buildLiveIframe(`<div id="a">t</div><div id="a-split">t</div>`);
|
|
549
|
-
const reloadPreview = vi.fn();
|
|
550
|
-
const files = {
|
|
551
|
-
[ROOT]: {
|
|
552
|
-
previous: wrap(`<div id="a">t</div><div id="a-split">t</div>`),
|
|
553
|
-
restored: wrap(`<div id="a">t</div>`),
|
|
554
|
-
},
|
|
555
|
-
};
|
|
556
|
-
expect(applyUndoRestoreToPreview(iframe, ROOT, files, 3, reloadPreview)).toBe("full");
|
|
557
|
-
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
it("full-reloads when the restore touches a sub-comp, not the active comp", () => {
|
|
561
|
-
const { iframe } = buildLiveIframe(`<div id="a">t</div>`);
|
|
562
|
-
const reloadPreview = vi.fn();
|
|
563
|
-
const files = { "scenes/intro.html": { previous: "a", restored: "b" } };
|
|
564
|
-
expect(applyUndoRestoreToPreview(iframe, ROOT, files, 3, reloadPreview)).toBe("full");
|
|
565
|
-
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
566
|
-
});
|
|
567
|
-
});
|
|
@@ -90,7 +90,7 @@ function isGsapScript(text: string): boolean {
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function findGsapScriptElements(doc: Document): HTMLScriptElement[] {
|
|
93
|
+
export function findGsapScriptElements(doc: Document): HTMLScriptElement[] {
|
|
94
94
|
const results: HTMLScriptElement[] = [];
|
|
95
95
|
const scripts = doc.querySelectorAll<HTMLScriptElement>("script:not([src])");
|
|
96
96
|
for (const script of scripts) {
|
|
@@ -102,8 +102,9 @@ function findGsapScriptElements(doc: Document): HTMLScriptElement[] {
|
|
|
102
102
|
/**
|
|
103
103
|
* Extract the GSAP timeline script text from a serialized HTML document, for
|
|
104
104
|
* feeding into applySoftReload. Returns null when zero or multiple GSAP scripts
|
|
105
|
-
* are present (ambiguous —
|
|
106
|
-
*
|
|
105
|
+
* are present (ambiguous — a serialized snapshot can't say WHICH script a
|
|
106
|
+
* single rewritten text corresponds to; caller should fall back to a full
|
|
107
|
+
* reload), matching applySoftReload's own single-script requirement.
|
|
107
108
|
*/
|
|
108
109
|
export function extractGsapScriptText(html: string): string | null {
|
|
109
110
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
@@ -183,157 +184,53 @@ export interface SoftReloadOptions {
|
|
|
183
184
|
authoredHtml?: string;
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// Strip id'd elements to bare `id` and blank GSAP scripts, in place: docs that
|
|
201
|
-
// differ only in id'd attributes/inline-style/script text normalize equal; any
|
|
202
|
-
// residual difference is beyond soft-reload's reach → caller full-reloads.
|
|
203
|
-
function normalizeSoftResidual(doc: Document): void {
|
|
204
|
-
for (const el of doc.querySelectorAll("[id]")) {
|
|
205
|
-
const id = el.getAttribute("id");
|
|
206
|
-
for (const name of [...el.getAttributeNames()]) {
|
|
207
|
-
if (name !== "id") el.removeAttribute(name);
|
|
208
|
-
}
|
|
209
|
-
if (id) el.setAttribute("id", id);
|
|
210
|
-
}
|
|
211
|
-
for (const script of findGsapScriptElements(doc)) script.textContent = "";
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Soft-reloadable iff the docs differ SOLELY in id'd-element attributes/inline
|
|
215
|
-
// style and/or the GSAP script; returns the changed ids to sync onto the live
|
|
216
|
-
// DOM. Structural/text diffs → null → the caller full-reloads. Pure.
|
|
217
|
-
export function diffSoftReloadableRestore(
|
|
218
|
-
previous: string,
|
|
219
|
-
restored: string,
|
|
220
|
-
): { changedElementIds: string[] } | null {
|
|
221
|
-
let prevDoc: Document;
|
|
222
|
-
let nextDoc: Document;
|
|
223
|
-
try {
|
|
224
|
-
prevDoc = new DOMParser().parseFromString(previous, "text/html");
|
|
225
|
-
nextDoc = new DOMParser().parseFromString(restored, "text/html");
|
|
226
|
-
} catch {
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
const prevById = idElementMap(prevDoc);
|
|
230
|
-
const nextById = idElementMap(nextDoc);
|
|
231
|
-
// A different id set means an element was added or removed (e.g. a split, a
|
|
232
|
-
// delete) — structural, so soft-reload can't express it.
|
|
233
|
-
if (prevById.size !== nextById.size) return null;
|
|
234
|
-
const changedElementIds: string[] = [];
|
|
235
|
-
for (const [id, nextEl] of nextById) {
|
|
236
|
-
const prevEl = prevById.get(id);
|
|
237
|
-
if (!prevEl || prevEl.tagName !== nextEl.tagName) return null;
|
|
238
|
-
// A change inside the element (text / children) is out of soft scope; only
|
|
239
|
-
// its own attributes may differ. (GSAP scripts are handled via re-run.)
|
|
240
|
-
if (prevEl.innerHTML !== nextEl.innerHTML) return null;
|
|
241
|
-
if (prevEl.outerHTML !== nextEl.outerHTML) changedElementIds.push(id);
|
|
242
|
-
}
|
|
243
|
-
// Confirm nothing OUTSIDE id'd-element attributes and GSAP scripts changed.
|
|
244
|
-
normalizeSoftResidual(prevDoc);
|
|
245
|
-
normalizeSoftResidual(nextDoc);
|
|
246
|
-
if (prevDoc.documentElement.outerHTML !== nextDoc.documentElement.outerHTML) return null;
|
|
247
|
-
return { changedElementIds };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/** Copy every attribute from `source` onto the live `target`, dropping extras. */
|
|
251
|
-
function syncElementAttributes(target: Element, source: Element): void {
|
|
252
|
-
for (const name of [...target.getAttributeNames()]) {
|
|
253
|
-
if (!source.hasAttribute(name)) target.removeAttribute(name);
|
|
254
|
-
}
|
|
255
|
-
for (const name of source.getAttributeNames()) {
|
|
256
|
-
target.setAttribute(name, source.getAttribute(name) ?? "");
|
|
257
|
-
}
|
|
187
|
+
/**
|
|
188
|
+
* The soft reload's finalization step, shared with the rebind-only preview sync
|
|
189
|
+
* below: seek → force timeline rebind → reapply studio manual edits.
|
|
190
|
+
*
|
|
191
|
+
* Seek BEFORE rebind: __hfForceTimelineRebind's own internal force-render
|
|
192
|
+
* (see init.ts) renders the freshly-created timeline at whatever the
|
|
193
|
+
* runtime's internal scrub position already is, not at whatever we pass
|
|
194
|
+
* here afterward — a redundant seek() call after rebind can be a GSAP
|
|
195
|
+
* no-op if the timeline already reports being at that time internally.
|
|
196
|
+
*/
|
|
197
|
+
function finalizeSoftReload(win: IframeWindow, currentTime: number): void {
|
|
198
|
+
win.__player?.seek?.(currentTime);
|
|
199
|
+
win.__hfForceTimelineRebind?.();
|
|
200
|
+
win.__hfStudioManualEditsApply?.();
|
|
258
201
|
}
|
|
259
202
|
|
|
260
203
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
204
|
+
* Run ONLY applySoftReload's finalization (seek → __hfForceTimelineRebind →
|
|
205
|
+
* manual-edits reapply) against the live iframe — executing NO scripts and
|
|
206
|
+
* touching NO script elements. `__hfForceTimelineRebind` makes the runtime
|
|
207
|
+
* re-derive every clip's visibility window from the live DOM's `data-start` /
|
|
208
|
+
* `data-duration` attributes (init.ts: bindRootTimelineIfAvailable +
|
|
209
|
+
* syncTimedElementVisibility), so this is the flashless sync for a timing edit
|
|
210
|
+
* whose attributes were already live-patched and whose GSAP scripts are
|
|
211
|
+
* unchanged (`window.__timelines` still valid). Works for compositions with
|
|
212
|
+
* zero GSAP scripts too — the rebind hook is installed unconditionally by the
|
|
213
|
+
* runtime, independent of any animation library.
|
|
265
214
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* 1. Each changed element's attribute surface (inline style, data-start /
|
|
269
|
-
* -duration, the studio manual-offset props + flags) is synced onto the live
|
|
270
|
-
* element — so a canvas-position revert lands on the live DOM the runtime's
|
|
271
|
-
* seek-reapply reads from, not just on disk.
|
|
272
|
-
* 2. The restored GSAP script is re-run in place via applySoftReload, which
|
|
273
|
-
* re-seeks to `currentTime` (playhead-invariant) and re-folds manual edits.
|
|
274
|
-
* With no single script, the manual-edit reapply is invoked directly.
|
|
275
|
-
*
|
|
276
|
-
* Returns "soft" when applied in place, "full" when it escalated to reloadPreview
|
|
277
|
-
* (ineligible restore, missing target, or a permanent soft-reload failure).
|
|
215
|
+
* Returns false when the iframe/runtime hook is unavailable or the run threw —
|
|
216
|
+
* the caller should escalate to a full reload.
|
|
278
217
|
*/
|
|
279
|
-
export function
|
|
218
|
+
export function applySoftReloadFinalization(
|
|
280
219
|
iframe: HTMLIFrameElement | null,
|
|
281
|
-
activeCompPath: string | null,
|
|
282
|
-
files: Record<string, UndoRestoreFile> | undefined,
|
|
283
220
|
currentTime: number,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// Soft path only covers the single active-comp document in the root iframe.
|
|
288
|
-
if (!iframe || !activeCompPath || !files || paths.length !== 1 || paths[0] !== activeCompPath) {
|
|
289
|
-
reloadPreview();
|
|
290
|
-
return "full";
|
|
291
|
-
}
|
|
292
|
-
const doc = iframe.contentDocument;
|
|
293
|
-
const win = iframe.contentWindow as IframeWindow | null;
|
|
294
|
-
if (!doc || !win) {
|
|
295
|
-
reloadPreview();
|
|
296
|
-
return "full";
|
|
297
|
-
}
|
|
298
|
-
const { previous, restored } = files[activeCompPath]!;
|
|
299
|
-
const diff = diffSoftReloadableRestore(previous, restored);
|
|
300
|
-
if (!diff) {
|
|
301
|
-
reloadPreview();
|
|
302
|
-
return "full";
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Sync each changed element's attributes onto the live DOM from the restored
|
|
306
|
-
// markup, so the runtime's seek-reapply (which reads inline offset props off
|
|
307
|
-
// the live element) folds the REVERTED values, not the stale current ones.
|
|
308
|
-
const restoredById = idElementMap(new DOMParser().parseFromString(restored, "text/html"));
|
|
309
|
-
for (const id of diff.changedElementIds) {
|
|
310
|
-
const liveEl = doc.getElementById(id);
|
|
311
|
-
const restoredEl = restoredById.get(id);
|
|
312
|
-
if (liveEl && restoredEl) syncElementAttributes(liveEl, restoredEl);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const script = extractGsapScriptText(restored);
|
|
316
|
-
if (script) {
|
|
317
|
-
const result = applySoftReload(iframe, script, {
|
|
318
|
-
onAsyncFailure: reloadPreview,
|
|
319
|
-
currentTimeOverride: currentTime,
|
|
320
|
-
});
|
|
321
|
-
if (result === "cannot-soft-reload") {
|
|
322
|
-
reloadPreview();
|
|
323
|
-
return "full";
|
|
324
|
-
}
|
|
325
|
-
return "soft";
|
|
326
|
-
}
|
|
327
|
-
// No single GSAP script to re-run — the change was pure attribute/style. Re-fold
|
|
328
|
-
// manual edits and hold the playhead so the synced attributes take visible effect.
|
|
221
|
+
): boolean {
|
|
222
|
+
const win = iframe?.contentWindow as IframeWindow | null;
|
|
223
|
+
if (!win?.__hfForceTimelineRebind) return false;
|
|
329
224
|
try {
|
|
330
|
-
win.
|
|
331
|
-
|
|
225
|
+
if (win.__hfSuppressSceneMutations) {
|
|
226
|
+
win.__hfSuppressSceneMutations(() => finalizeSoftReload(win, currentTime));
|
|
227
|
+
} else {
|
|
228
|
+
finalizeSoftReload(win, currentTime);
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
332
231
|
} catch {
|
|
333
|
-
|
|
334
|
-
return "full";
|
|
232
|
+
return false;
|
|
335
233
|
}
|
|
336
|
-
return "soft";
|
|
337
234
|
}
|
|
338
235
|
|
|
339
236
|
export function applySoftReload(
|
|
@@ -523,14 +420,7 @@ export function applySoftReload(
|
|
|
523
420
|
const s = doc.createElement("script");
|
|
524
421
|
s.textContent = `(function(){${scriptText}\n})();`;
|
|
525
422
|
doc.body.appendChild(s);
|
|
526
|
-
|
|
527
|
-
// (see init.ts) renders the freshly-created timeline at whatever the
|
|
528
|
-
// runtime's internal scrub position already is, not at whatever we pass
|
|
529
|
-
// here afterward — a redundant seek() call after rebind can be a GSAP
|
|
530
|
-
// no-op if the timeline already reports being at that time internally.
|
|
531
|
-
win.__player?.seek?.(currentTime);
|
|
532
|
-
win.__hfForceTimelineRebind?.();
|
|
533
|
-
win.__hfStudioManualEditsApply?.();
|
|
423
|
+
finalizeSoftReload(win, currentTime);
|
|
534
424
|
};
|
|
535
425
|
|
|
536
426
|
const needsMotionPath = /motionPath\s*[:{]/.test(scriptText);
|