@hyperframes/studio 0.8.103 → 0.8.105
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-DG795Hwu.js → hyperframes-player-tnfCiVdR.js} +1 -1
- package/dist/assets/{index-BiEtDkae.js → index-Ca6LZlVb.js} +1 -1
- package/dist/assets/index-Cre9X4RS.css +1 -0
- package/dist/assets/{index-BPJRf2X9.js → index-DEDhPmPD.js} +215 -215
- package/dist/assets/{index-DuCZ3pQw.js → index-zpe332hb.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-QADHJFDF.js} +2 -2
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +181 -168
- package/dist/index.html +2 -2
- package/dist/index.js +1018 -919
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.js +2 -2
- package/package.json +8 -8
- package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
- package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
- package/src/captions/components/CaptionTimeline.tsx +3 -3
- package/src/components/AskAgentModal.tsx +1 -1
- package/src/components/LintModal.tsx +35 -35
- package/src/components/StudioErrorBoundary.tsx +1 -1
- package/src/components/StudioHeader.dom.test.tsx +4 -4
- package/src/components/StudioHeader.tsx +2 -1
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/TimelineToolPicker.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +11 -11
- package/src/components/dock/Dock.tsx +1 -1
- package/src/components/dock/dock.css +9 -1
- package/src/components/editor/AnimationCard.tsx +5 -5
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/BorderRadiusEditor.tsx +8 -15
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +5 -6
- package/src/components/editor/DomEditRotateHandle.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +14 -19
- package/src/components/editor/EaseModeControls.tsx +2 -2
- package/src/components/editor/EaseParamFields.tsx +8 -8
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/FileTreeIcons.tsx +47 -26
- package/src/components/editor/FileTreeNodes.tsx +11 -8
- package/src/components/editor/GestureRecordControl.tsx +3 -3
- package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/InlineTextToolbar.tsx +4 -4
- package/src/components/editor/InspectorHeaderActions.tsx +1 -1
- package/src/components/editor/KeyframeDiamond.tsx +6 -4
- package/src/components/editor/KeyframeNavigation.tsx +4 -4
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanel.tsx +3 -3
- package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
- package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +5 -5
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +46 -6
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayGestures.ts +13 -19
- package/src/components/editor/domEditOverlayStartGesture.test.ts +170 -13
- package/src/components/editor/domEditOverlayStartGesture.ts +15 -25
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/manualEdits.test.ts +3 -9
- package/src/components/editor/manualEdits.ts +0 -1
- package/src/components/editor/manualEditsDom.ts +3 -13
- package/src/components/editor/manualEditsDomPatches.test.ts +3 -131
- package/src/components/editor/manualEditsDomPatches.ts +1 -53
- package/src/components/editor/manualEditsSnapshot.ts +3 -0
- package/src/components/editor/manualEditsTypes.ts +2 -0
- package/src/components/editor/manualOffsetDrag.test.ts +41 -0
- package/src/components/editor/manualOffsetDrag.ts +6 -27
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +42 -6
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
- package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
- package/src/components/editor/propertyPanelFill.tsx +6 -6
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +2 -2
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +2 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
- package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +29 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +8 -5
- package/src/components/editor/propertyPanelTypes.ts +1 -0
- package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/rotateInlineSave.test.ts +111 -0
- package/src/components/editor/rotationDraft.test.ts +111 -0
- package/src/components/editor/rotationDraft.ts +145 -0
- package/src/components/editor/useDomEditNudge.test.tsx +33 -0
- package/src/components/editor/useDomEditNudge.ts +6 -2
- package/src/components/editor/useDomEditOverlayGestures.ts +22 -24
- package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
- package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
- package/src/components/nle/AudioMeterStrip.tsx +2 -2
- package/src/components/nle/NLEPreview.test.ts +8 -4
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/PreviewPoster.tsx +29 -1
- package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
- package/src/components/panels/SlideshowPanel.tsx +4 -4
- package/src/components/panels/SlideshowSubPanels.tsx +19 -21
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -6
- package/src/components/panels/VariablesRowAction.tsx +1 -1
- package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +3 -3
- package/src/components/sidebar/AssetCard.tsx +3 -3
- package/src/components/sidebar/AssetContextMenu.tsx +7 -7
- package/src/components/sidebar/AssetsTab.tsx +6 -6
- package/src/components/sidebar/AudioRow.tsx +5 -3
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
- package/src/components/sidebar/CompositionsTab.tsx +65 -12
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.tsx +3 -3
- package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
- package/src/components/timelineToolbarStyles.ts +3 -3
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/HyperframesLogo.tsx +19 -18
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Menu.tsx +4 -2
- package/src/hooks/draggedGsapPosition.ts +28 -0
- package/src/hooks/elementOffsetStager.ts +34 -13
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +36 -2
- package/src/hooks/plainRotation.test.ts +122 -0
- package/src/hooks/plainRotation.ts +42 -0
- package/src/hooks/useClipboard.source.test.ts +42 -4
- package/src/hooks/useClipboard.ts +13 -3
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.ts +5 -5
- package/src/hooks/useDomGeometryCommit.test.tsx +79 -1
- package/src/hooks/useDomGeometryCommit.ts +16 -17
- package/src/hooks/useDomGeometryCommits.test.tsx +233 -33
- package/src/hooks/useDomGeometryCommits.ts +27 -80
- package/src/hooks/useGsapAwareEditing.test.tsx +194 -11
- package/src/hooks/useGsapAwareEditing.ts +43 -42
- package/src/hooks/useInlineTextEdit.test.tsx +33 -3
- package/src/hooks/useInlineTextEdit.ts +2 -2
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/CompositionThumbnail.tsx +23 -11
- package/src/player/components/EditModal.tsx +6 -6
- package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +3 -3
- package/src/player/components/ShortcutsPanel.tsx +8 -20
- package/src/player/components/TimelineCanvas.tsx +1 -1
- package/src/player/components/TimelineClip.tsx +2 -2
- package/src/player/components/TimelineEmptyState.tsx +2 -2
- package/src/player/components/TimelineLanes.tsx +0 -1
- package/src/player/components/TrackClipCount.tsx +1 -1
- package/src/player/components/VolumeControl.tsx +1 -1
- package/src/player/components/timelineClipChildren.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +6 -22
- package/src/player/components/trackHeaderLabelRows.tsx +16 -16
- package/src/player/lib/thumbnailScheduler.ts +4 -5
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +0 -9
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +204 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +2 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
- /package/dist/{chunk-HIGHVWYD.js.map → chunk-QADHJFDF.js.map} +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
5
|
+
import {
|
|
6
|
+
applyStudioRotation,
|
|
7
|
+
reapplyPositionEditsAfterSeek,
|
|
8
|
+
} from "../components/editor/manualEdits";
|
|
9
|
+
import { STUDIO_ROTATION_ATTR } from "../components/editor/manualEditsTypes";
|
|
10
|
+
import type { ElementOffsetStagerDeps } from "./elementOffsetStager";
|
|
11
|
+
import { savePlainRotation } from "./plainRotation";
|
|
12
|
+
import { readShownRotation } from "../components/editor/plainTranslate";
|
|
13
|
+
|
|
14
|
+
function rotate(
|
|
15
|
+
element: HTMLElement,
|
|
16
|
+
angle: number,
|
|
17
|
+
options: { save?: () => Promise<void>; readOnlyPreview?: boolean } = {},
|
|
18
|
+
) {
|
|
19
|
+
element.id = "box";
|
|
20
|
+
document.body.append(element);
|
|
21
|
+
const commitPositionPatchToHtml = vi.fn<ElementOffsetStagerDeps["commitPositionPatchToHtml"]>(
|
|
22
|
+
options.save ?? (() => Promise.resolve()),
|
|
23
|
+
);
|
|
24
|
+
const selection = { element, id: "box", selector: "#box" } as unknown as DomEditSelection;
|
|
25
|
+
const saved = savePlainRotation(
|
|
26
|
+
{ commitPositionPatchToHtml, readOnlyPreview: options.readOnlyPreview },
|
|
27
|
+
selection,
|
|
28
|
+
{ angle },
|
|
29
|
+
);
|
|
30
|
+
return { saved, commitPositionPatchToHtml, selection };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("savePlainRotation", () => {
|
|
34
|
+
it("saves the element's own rotate, less what its transform turns, with no Studio marks", async () => {
|
|
35
|
+
const element = document.createElement("div");
|
|
36
|
+
element.style.transform = "rotate(10deg)";
|
|
37
|
+
const { saved, commitPositionPatchToHtml, selection } = rotate(element, 55);
|
|
38
|
+
await saved;
|
|
39
|
+
expect(element.style.getPropertyValue("rotate")).toBe("45deg");
|
|
40
|
+
expect(commitPositionPatchToHtml).toHaveBeenCalledWith(
|
|
41
|
+
selection,
|
|
42
|
+
[{ type: "inline-style", property: "rotate", value: "45deg" }],
|
|
43
|
+
expect.objectContaining({ label: "Rotate layer" }),
|
|
44
|
+
);
|
|
45
|
+
expect(element.hasAttribute(STUDIO_ROTATION_ATTR)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("drops a legacy Studio rotation in the same write, so a seek does not put its angle back", async () => {
|
|
49
|
+
const element = document.createElement("div");
|
|
50
|
+
applyStudioRotation(element, { angle: 15 });
|
|
51
|
+
const { saved, commitPositionPatchToHtml } = rotate(element, 40);
|
|
52
|
+
await saved;
|
|
53
|
+
const patches = commitPositionPatchToHtml.mock.calls[0]![1];
|
|
54
|
+
expect(patches).toContainEqual({
|
|
55
|
+
type: "attribute",
|
|
56
|
+
property: STUDIO_ROTATION_ATTR,
|
|
57
|
+
value: null,
|
|
58
|
+
});
|
|
59
|
+
expect(patches.at(-1)).toEqual({ type: "inline-style", property: "rotate", value: "40deg" });
|
|
60
|
+
reapplyPositionEditsAfterSeek(document);
|
|
61
|
+
expect(element.style.getPropertyValue("rotate")).toBe("40deg");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("puts the live rotate back when the save fails", async () => {
|
|
65
|
+
const element = document.createElement("div");
|
|
66
|
+
element.style.setProperty("rotate", "12deg");
|
|
67
|
+
const { saved } = rotate(element, 30, { save: () => Promise.reject(new Error("offline")) });
|
|
68
|
+
await expect(saved).rejects.toThrow("offline");
|
|
69
|
+
expect(element.style.getPropertyValue("rotate")).toBe("12deg");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("writes nothing in a read-only preview", async () => {
|
|
73
|
+
const element = document.createElement("div");
|
|
74
|
+
const { saved, commitPositionPatchToHtml } = rotate(element, 30, { readOnlyPreview: true });
|
|
75
|
+
await saved;
|
|
76
|
+
expect(element.style.getPropertyValue("rotate")).toBe("");
|
|
77
|
+
expect(commitPositionPatchToHtml).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("saves a transform-centred element's turn in its transform and puts it back on a failed save", async () => {
|
|
81
|
+
const element = document.createElement("div");
|
|
82
|
+
element.style.transform = "translate(-120px, -80px)";
|
|
83
|
+
const { saved, commitPositionPatchToHtml } = rotate(element, 25, {
|
|
84
|
+
save: () => Promise.reject(new Error("offline")),
|
|
85
|
+
});
|
|
86
|
+
expect(commitPositionPatchToHtml.mock.calls[0]![1]).toEqual([
|
|
87
|
+
{
|
|
88
|
+
type: "inline-style",
|
|
89
|
+
property: "transform",
|
|
90
|
+
value: "translate(-120px, -80px) rotate(25deg)",
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
await expect(saved).rejects.toThrow("offline");
|
|
94
|
+
expect(element.style.getPropertyValue("transform")).toBe("translate(-120px, -80px)");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("makes an inline element inline-block, as a turn needs, and puts it back on a failed save", async () => {
|
|
98
|
+
const element = document.createElement("span");
|
|
99
|
+
element.style.setProperty("display", "inline");
|
|
100
|
+
const { saved, commitPositionPatchToHtml } = rotate(element, 20, {
|
|
101
|
+
save: () => Promise.reject(new Error("offline")),
|
|
102
|
+
});
|
|
103
|
+
expect(commitPositionPatchToHtml.mock.calls[0]![1]).toEqual([
|
|
104
|
+
{ type: "inline-style", property: "display", value: "inline-block" },
|
|
105
|
+
{ type: "inline-style", property: "rotate", value: "20deg" },
|
|
106
|
+
]);
|
|
107
|
+
await expect(saved).rejects.toThrow("offline");
|
|
108
|
+
expect(element.style.getPropertyValue("display")).toBe("inline");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("readShownRotation", () => {
|
|
113
|
+
it("shows an element's own CSS turn, and GSAP's legacy reading once GSAP turns it", () => {
|
|
114
|
+
const element = document.createElement("div");
|
|
115
|
+
document.body.append(element);
|
|
116
|
+
element.style.setProperty("rotate", "25deg");
|
|
117
|
+
expect(readShownRotation(element)).toEqual({ angle: 25 });
|
|
118
|
+
Object.assign(element, { _gsap: { renderTransform: () => {} } });
|
|
119
|
+
applyStudioRotation(element, { angle: 15 });
|
|
120
|
+
expect(readShownRotation(element)).toEqual({ angle: 15 });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
2
|
+
import { captureStudioRotation, clearStudioRotation } from "../components/editor/manualEdits";
|
|
3
|
+
import { buildClearRotationPatches } from "../components/editor/manualEditsDomPatches";
|
|
4
|
+
import { STUDIO_ROTATION_ATTR } from "../components/editor/manualEditsTypes";
|
|
5
|
+
import {
|
|
6
|
+
applyCssRotation,
|
|
7
|
+
restorePlainRotation,
|
|
8
|
+
type RotationCommit,
|
|
9
|
+
} from "../components/editor/rotationDraft";
|
|
10
|
+
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
11
|
+
import type { ElementOffsetStagerDeps } from "./elementOffsetStager";
|
|
12
|
+
|
|
13
|
+
let plainRotateCounter = 0;
|
|
14
|
+
|
|
15
|
+
/** GSAP does not turn the element: `next` is its whole angle, drawn and saved as its CSS turn. */
|
|
16
|
+
export function savePlainRotation(
|
|
17
|
+
{ commitPositionPatchToHtml, readOnlyPreview }: Omit<ElementOffsetStagerDeps, "showToast">,
|
|
18
|
+
selection: DomEditSelection,
|
|
19
|
+
next: RotationCommit,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
if (readOnlyPreview) return Promise.resolve();
|
|
22
|
+
const { element } = selection;
|
|
23
|
+
const before = captureStudioRotation(element);
|
|
24
|
+
// Legacy marks go in the same write, or a seek puts their angle back; clearing them voids the press read.
|
|
25
|
+
const patches: PatchOperation[] = element.hasAttribute(STUDIO_ROTATION_ATTR)
|
|
26
|
+
? buildClearRotationPatches(element)
|
|
27
|
+
: [];
|
|
28
|
+
if (patches.length) clearStudioRotation(element);
|
|
29
|
+
const drawn = applyCssRotation(element, next.angle, patches.length ? undefined : next.plain);
|
|
30
|
+
patches.push(...drawn);
|
|
31
|
+
const turn = drawn.at(-1)!;
|
|
32
|
+
return commitPositionPatchToHtml(selection, patches, {
|
|
33
|
+
label: "Rotate layer",
|
|
34
|
+
coalesceKey: `rotate:${++plainRotateCounter}`,
|
|
35
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
36
|
+
}).catch((error) => {
|
|
37
|
+
if (element.style.getPropertyValue(turn.property) === turn.value) {
|
|
38
|
+
restorePlainRotation(element, before);
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -52,12 +52,16 @@ afterEach(() => {
|
|
|
52
52
|
|
|
53
53
|
// The preview page: the saved markup with the preview's in-memory ids, a sub-composition
|
|
54
54
|
// mounted inline, and the runtime's hide on both clips.
|
|
55
|
-
function mountPreview(
|
|
55
|
+
function mountPreview(
|
|
56
|
+
sub: string,
|
|
57
|
+
bundle: (host: Element) => void,
|
|
58
|
+
saved = SAVED,
|
|
59
|
+
): HTMLIFrameElement {
|
|
56
60
|
const iframe = document.createElement("iframe");
|
|
57
61
|
document.body.appendChild(iframe);
|
|
58
62
|
const doc = iframe.contentDocument as Document;
|
|
59
63
|
doc.open();
|
|
60
|
-
doc.write(ensureHfIds(
|
|
64
|
+
doc.write(ensureHfIds(saved));
|
|
61
65
|
doc.close();
|
|
62
66
|
const parsed = new DOMParser().parseFromString(ensureHfIds(sub), "text/html");
|
|
63
67
|
const subRoot = (parsed.querySelector("template") as HTMLTemplateElement).content
|
|
@@ -93,14 +97,15 @@ function mountClipboard(
|
|
|
93
97
|
domSelection: DomEditSelection | null = null,
|
|
94
98
|
sub = SUB,
|
|
95
99
|
bundle: (host: Element) => void = () => {},
|
|
100
|
+
saved = SAVED,
|
|
96
101
|
) {
|
|
97
|
-
const files: Record<string, string> = { "index.html":
|
|
102
|
+
const files: Record<string, string> = { "index.html": saved, "compositions/sub.html": sub };
|
|
98
103
|
const fail = { on: false };
|
|
99
104
|
const delayMs: Record<string, number> = {};
|
|
100
105
|
const domEditSave = { pending: Promise.resolve() };
|
|
101
106
|
const domSelectionRef = { current: domSelection };
|
|
102
107
|
stubFiles(files, fail, delayMs);
|
|
103
|
-
const iframe = mountPreview(sub, bundle);
|
|
108
|
+
const iframe = mountPreview(sub, bundle, saved);
|
|
104
109
|
const writes: string[] = [];
|
|
105
110
|
const deleted: string[] = [];
|
|
106
111
|
const writeProjectFile = async (_path: string, content: string) => {
|
|
@@ -248,6 +253,39 @@ const SUB_SELECTION = {
|
|
|
248
253
|
sourceFile: "compositions/sub.html",
|
|
249
254
|
} as DomEditSelection;
|
|
250
255
|
|
|
256
|
+
describe("paste of an element styled by its id", () => {
|
|
257
|
+
it("gives the copy the look its original has", async () => {
|
|
258
|
+
clearSelection();
|
|
259
|
+
const saved = SAVED.replace(
|
|
260
|
+
"<body>",
|
|
261
|
+
"<body><style>#badge { color: rgb(200, 30, 40); letter-spacing: 3px; }</style>",
|
|
262
|
+
).replace("</h1>", '</h1><p id="badge">New</p>');
|
|
263
|
+
const selection = {
|
|
264
|
+
hfId: stampedHfId(saved, "#badge"),
|
|
265
|
+
selector: "#badge",
|
|
266
|
+
selectorIndex: 0,
|
|
267
|
+
sourceFile: "index.html",
|
|
268
|
+
} as DomEditSelection;
|
|
269
|
+
const { clipboard, writes } = mountClipboard(selection, SUB, () => {}, saved);
|
|
270
|
+
clipboard().handleCopy();
|
|
271
|
+
await clipboard().handlePaste();
|
|
272
|
+
const page = document.createElement("iframe");
|
|
273
|
+
document.body.appendChild(page);
|
|
274
|
+
const doc = page.contentDocument as Document;
|
|
275
|
+
doc.open();
|
|
276
|
+
doc.write(writes[0] ?? "");
|
|
277
|
+
doc.close();
|
|
278
|
+
const look = (id: string) => {
|
|
279
|
+
const style = (page.contentWindow as Window).getComputedStyle(
|
|
280
|
+
doc.getElementById(id) as Element,
|
|
281
|
+
);
|
|
282
|
+
return { color: style.color, letterSpacing: style.letterSpacing };
|
|
283
|
+
};
|
|
284
|
+
expect(look("badge")).toEqual({ color: "rgb(200, 30, 40)", letterSpacing: "3px" });
|
|
285
|
+
expect(look("badge-2")).toEqual(look("badge"));
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
251
289
|
describe("copy of a sub-composition clip", () => {
|
|
252
290
|
it("rebases its relative asset paths to the project root, as the preview does", async () => {
|
|
253
291
|
clearSelection();
|
|
@@ -160,10 +160,17 @@ const hfIdsInFile = (content: string) =>
|
|
|
160
160
|
export function pasteElementHtml(
|
|
161
161
|
content: string,
|
|
162
162
|
payload: { html: string; originSelector?: string; originSelectorIndex?: number },
|
|
163
|
+
fromThisFile = false,
|
|
163
164
|
): string {
|
|
164
165
|
const reminted = remintHfIds(payload.html, new DOMParser(), hfIdsInFile(content));
|
|
165
166
|
const deduped = deduplicateIds(reminted, collectHtmlIds(content));
|
|
166
|
-
|
|
167
|
+
const result = insertAsSibling(
|
|
168
|
+
content,
|
|
169
|
+
deduped,
|
|
170
|
+
payload.originSelector,
|
|
171
|
+
payload.originSelectorIndex,
|
|
172
|
+
);
|
|
173
|
+
return fromThisFile ? carryLook(result, renamedIds(reminted, deduped), 0) : result;
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
/** Shared insertion path for paste and duplicate, anchored at the playhead or the selection's end. Returns the
|
|
@@ -324,6 +331,7 @@ export function useClipboard({
|
|
|
324
331
|
kind: "dom-element",
|
|
325
332
|
html: savedMarkupElseLive(parseSavedSource(content), live, sourceFile),
|
|
326
333
|
sourceFile,
|
|
334
|
+
projectId: projectIdRef.current ?? undefined,
|
|
327
335
|
originSelector: domSelection.selector,
|
|
328
336
|
originSelectorIndex: domSelection.selectorIndex,
|
|
329
337
|
};
|
|
@@ -373,15 +381,17 @@ export function useClipboard({
|
|
|
373
381
|
const targetPath = activeCompPath || "index.html";
|
|
374
382
|
try {
|
|
375
383
|
let pastedIds: string[] = [];
|
|
384
|
+
const fromThisFile = payload.sourceFile === targetPath && payload.projectId === pid;
|
|
376
385
|
const paste = (originalContent: string) => {
|
|
377
|
-
if (payload.kind !== "timeline-clip")
|
|
386
|
+
if (payload.kind !== "timeline-clip")
|
|
387
|
+
return pasteElementHtml(originalContent, payload, fromThisFile);
|
|
378
388
|
const { currentTime, elements } = usePlayerStore.getState();
|
|
379
389
|
const pasted = pasteTimelineClips(
|
|
380
390
|
originalContent,
|
|
381
391
|
payload.clips,
|
|
382
392
|
currentTime,
|
|
383
393
|
elements,
|
|
384
|
-
|
|
394
|
+
fromThisFile,
|
|
385
395
|
);
|
|
386
396
|
pastedIds = pasted.ids;
|
|
387
397
|
// A clip pasted past the current composition end would exist in the
|
|
@@ -271,12 +271,10 @@ export function useDomEditCommits({
|
|
|
271
271
|
|
|
272
272
|
const {
|
|
273
273
|
stageElementPositionOffset,
|
|
274
|
-
handleDomPathOffsetCommit,
|
|
275
274
|
handleDomBoxSizeCommit,
|
|
276
275
|
handleDomRotationCommit,
|
|
277
276
|
handleDomManualEditsReset,
|
|
278
277
|
} = useDomGeometryCommits({
|
|
279
|
-
previewIframeRef,
|
|
280
278
|
showToast,
|
|
281
279
|
commitPositionPatchToHtml,
|
|
282
280
|
readOnlyPreview,
|
|
@@ -316,7 +314,6 @@ export function useDomEditCommits({
|
|
|
316
314
|
handleDomRemoveTextField,
|
|
317
315
|
stageElementPositionOffset,
|
|
318
316
|
commitPositionPatchToHtml,
|
|
319
|
-
handleDomPathOffsetCommit,
|
|
320
317
|
handleDomBoxSizeCommit,
|
|
321
318
|
handleDomRotationCommit,
|
|
322
319
|
handleDomManualEditsReset,
|
|
@@ -24,8 +24,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
|
24
24
|
import { membersForDelete, timelineElementsForDelete } from "./domEditDeleteMembers";
|
|
25
25
|
import type { RecordEditInput } from "./domEditDeleteMembers";
|
|
26
26
|
import type { DomEditTimelineParams } from "./useDomSelectionTypes";
|
|
27
|
-
// Re-exported: the delete rule lives in its own module
|
|
28
|
-
// own test) have always imported it from here.
|
|
27
|
+
// Re-exported: the delete rule lives in its own module; callers and its test import it from here.
|
|
29
28
|
export { membersForDelete };
|
|
30
29
|
|
|
31
30
|
export interface UseDomEditSessionParams extends DomEditTimelineParams {
|
|
@@ -232,6 +231,7 @@ export function useDomEditSession({
|
|
|
232
231
|
stageElementPositionOffset,
|
|
233
232
|
commitPositionPatchToHtml,
|
|
234
233
|
handleDomBoxSizeCommit,
|
|
234
|
+
handleDomRotationCommit: handleDomCssRotationCommit,
|
|
235
235
|
handleDomManualEditsReset,
|
|
236
236
|
handleDomEditElementsDelete,
|
|
237
237
|
handleDomZIndexReorderCommit,
|
|
@@ -256,9 +256,8 @@ export function useDomEditSession({
|
|
|
256
256
|
readOnlyPreview,
|
|
257
257
|
onTrySdkPersist: sdkSession
|
|
258
258
|
? (selection, operations, originalContent, targetPath, options) => {
|
|
259
|
-
// Decoupled tripwire,
|
|
260
|
-
//
|
|
261
|
-
// nodes); the paths let a cross-file edit skip instead of a false not-found.
|
|
259
|
+
// Decoupled tripwire, regardless of the cutover flag. originalContent lets the runtime-node
|
|
260
|
+
// filter drop hf-ids absent from source; the paths skip a cross-file edit, not a not-found.
|
|
262
261
|
runResolverShadow(sdkSession, selection.hfId, operations, originalContent, {
|
|
263
262
|
targetPath,
|
|
264
263
|
compositionPath: activeCompPath,
|
|
@@ -491,6 +490,7 @@ export function useDomEditSession({
|
|
|
491
490
|
trackGsapInteractionFailure,
|
|
492
491
|
stageElementPositionOffset,
|
|
493
492
|
handleDomBoxSizeCommit,
|
|
493
|
+
handleDomRotationCommit: handleDomCssRotationCommit,
|
|
494
494
|
commitPositionPatchToHtml,
|
|
495
495
|
addGsapAnimation,
|
|
496
496
|
convertToKeyframes,
|
|
@@ -168,7 +168,32 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
168
168
|
expect(element.style.getPropertyValue("translate")).toBe("130.5px 90px");
|
|
169
169
|
expect(calls.patches).toEqual([
|
|
170
170
|
expect.objectContaining({
|
|
171
|
-
operations: [
|
|
171
|
+
operations: [
|
|
172
|
+
{ type: "inline-style", property: "translate", value: "130.5px 90px" },
|
|
173
|
+
{ type: "attribute", property: "data-hf-studio-original-inline-translate", value: "" },
|
|
174
|
+
],
|
|
175
|
+
}),
|
|
176
|
+
]);
|
|
177
|
+
expect(calls.urls.filter((url) => url.includes("gsap"))).toEqual([]);
|
|
178
|
+
expect(recordEdit).toHaveBeenCalledTimes(1);
|
|
179
|
+
unmount();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("saves a GSAP-free resize as its inline size, with no GSAP script and no animation read", async () => {
|
|
183
|
+
const calls = stubPatchServer();
|
|
184
|
+
const { element, recordEdit, hook, unmount } = renderHost();
|
|
185
|
+
|
|
186
|
+
await expect(
|
|
187
|
+
hook().commitBoxSize(makeSelection("card", element), { width: 300, height: 90 }),
|
|
188
|
+
).resolves.toEqual({ ok: true });
|
|
189
|
+
|
|
190
|
+
expect(element.style.getPropertyValue("width")).toBe("300px");
|
|
191
|
+
expect(calls.patches).toEqual([
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
operations: expect.arrayContaining([
|
|
194
|
+
{ type: "inline-style", property: "width", value: "300px" },
|
|
195
|
+
{ type: "inline-style", property: "height", value: "90px" },
|
|
196
|
+
]),
|
|
172
197
|
}),
|
|
173
198
|
]);
|
|
174
199
|
expect(calls.urls.filter((url) => url.includes("gsap"))).toEqual([]);
|
|
@@ -188,6 +213,57 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
188
213
|
unmount();
|
|
189
214
|
});
|
|
190
215
|
|
|
216
|
+
it("refuses a CSS move GSAP has folded into its x/y, writes nothing, and hands the translate back", async () => {
|
|
217
|
+
const calls = stubPatchServer();
|
|
218
|
+
const { element, hook, unmount } = renderHost();
|
|
219
|
+
element.style.setProperty("translate", "none");
|
|
220
|
+
Object.assign(element, { _gsap: { renderTransform: () => {}, x: "94px", y: "66px" } });
|
|
221
|
+
const set = vi.fn();
|
|
222
|
+
Object.assign(element.ownerDocument.defaultView!, { gsap: { set } });
|
|
223
|
+
|
|
224
|
+
await expect(
|
|
225
|
+
hook().commitPathOffset(
|
|
226
|
+
makeSelection("card", element),
|
|
227
|
+
{ x: 1, y: 2 },
|
|
228
|
+
{ plainTranslate: true },
|
|
229
|
+
),
|
|
230
|
+
).rejects.toThrow(/animation took over/);
|
|
231
|
+
delete (element.ownerDocument.defaultView as { gsap?: unknown }).gsap;
|
|
232
|
+
expect(calls.patches).toEqual([]);
|
|
233
|
+
expect(set).toHaveBeenCalledWith(element, { x: 0, y: 0, xPercent: 0, yPercent: 0 });
|
|
234
|
+
unmount();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("saves a CSS move GSAP has only parsed, with nothing folded into its x/y", async () => {
|
|
238
|
+
const calls = stubPatchServer();
|
|
239
|
+
const { element, hook, unmount } = renderHost();
|
|
240
|
+
Object.assign(element, {
|
|
241
|
+
_gsap: { renderTransform: () => {}, x: "0px", y: "0px", xPercent: 0 },
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await expect(
|
|
245
|
+
hook().commitPathOffset(
|
|
246
|
+
makeSelection("card", element),
|
|
247
|
+
{ x: 1, y: 2 },
|
|
248
|
+
{ plainTranslate: true },
|
|
249
|
+
),
|
|
250
|
+
).resolves.toEqual({ ok: true });
|
|
251
|
+
expect(calls.patches).toHaveLength(1);
|
|
252
|
+
unmount();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("leaves a later move's translate alone when an earlier move's save fails", async () => {
|
|
256
|
+
stubPatchServer(500);
|
|
257
|
+
const { element, hook, unmount } = renderHost();
|
|
258
|
+
element.style.setProperty("translate", "40px 30px");
|
|
259
|
+
|
|
260
|
+
const saving = hook().commitPathOffset(makeSelection("card", element), { x: 1, y: 2 });
|
|
261
|
+
element.style.setProperty("translate", "7px 8px");
|
|
262
|
+
await expect(saving).rejects.toThrow();
|
|
263
|
+
expect(element.style.getPropertyValue("translate")).toBe("7px 8px");
|
|
264
|
+
unmount();
|
|
265
|
+
});
|
|
266
|
+
|
|
191
267
|
it("keeps the same commits across renders, so the overlay's handlers stay put", () => {
|
|
192
268
|
stubServer();
|
|
193
269
|
const { hook, rerender, unmount } = renderHost();
|
|
@@ -216,6 +292,8 @@ describe("useDomGeometryCommit, from the package entry", () => {
|
|
|
216
292
|
const showToast = vi.fn();
|
|
217
293
|
const restore = vi.fn();
|
|
218
294
|
const { element, recordEdit, hook, unmount } = renderHost({ showToast });
|
|
295
|
+
// GSAP renders the card's transform, so its resize reads animations first.
|
|
296
|
+
Object.assign(element, { _gsap: { renderTransform: () => undefined } });
|
|
219
297
|
|
|
220
298
|
await expect(
|
|
221
299
|
hook().commitBoxSize(
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
1
2
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
3
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
4
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
DomEditGroupPathOffsetCommit,
|
|
7
|
+
MoveCommitOptions,
|
|
8
|
+
} from "../components/editor/DomEditOverlay";
|
|
5
9
|
import { isPreviewBooted } from "../player/store/playerStore";
|
|
6
10
|
import type { DomEditCommitOutcome } from "./domEditCommitRunner";
|
|
7
11
|
import type { UseDomStyleCommitOptions } from "./useDomStyleCommit";
|
|
@@ -13,8 +17,8 @@ import { useGsapCacheVersion } from "./useGsapTweenCache";
|
|
|
13
17
|
import { createDomEditSaveQueue } from "../utils/domEditSaveQueue";
|
|
14
18
|
import { useDomEditPersist } from "./useDomEditPersist";
|
|
15
19
|
import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit";
|
|
20
|
+
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
|
16
21
|
import { useMountEffect } from "./useMountEffect";
|
|
17
|
-
import { stageElementOffset } from "./elementOffsetStager";
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Studio's `Player` must show the project, with `beginTimelineSession(projectId)` run before it
|
|
@@ -29,7 +33,7 @@ export interface DomGeometryCommits {
|
|
|
29
33
|
commitPathOffset: (
|
|
30
34
|
selection: DomEditSelection,
|
|
31
35
|
next: { x: number; y: number },
|
|
32
|
-
modifiers?:
|
|
36
|
+
modifiers?: MoveCommitOptions,
|
|
33
37
|
) => Promise<DomEditCommitOutcome>;
|
|
34
38
|
commitGroupPathOffset: (updates: DomEditGroupPathOffsetCommit[]) => Promise<DomEditCommitOutcome>;
|
|
35
39
|
commitBoxSize: (
|
|
@@ -40,15 +44,13 @@ export interface DomGeometryCommits {
|
|
|
40
44
|
) => Promise<DomEditCommitOutcome>;
|
|
41
45
|
commitRotation: (
|
|
42
46
|
selection: DomEditSelection,
|
|
43
|
-
next:
|
|
47
|
+
next: RotationCommit,
|
|
44
48
|
) => Promise<DomEditCommitOutcome>;
|
|
45
49
|
waitForPendingSaves: () => Promise<void>;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
const noop = () => {};
|
|
49
53
|
const NO_SELECTED_ANIMATIONS: GsapAnimation[] = [];
|
|
50
|
-
// ponytail: unreachable, the GSAP writer always exists so a resize never takes the DOM route.
|
|
51
|
-
const noDomBoxSizeRoute = () => Promise.reject(new Error("Resize has no DOM route here"));
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Saves canvas moves, resizes and rotations through Studio's own GSAP-aware commits, for a host
|
|
@@ -107,16 +109,12 @@ export function useDomGeometryCommit({
|
|
|
107
109
|
},
|
|
108
110
|
[commitPositionPatchToHtml, queue],
|
|
109
111
|
);
|
|
110
|
-
const stageElementPositionOffset =
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
coalesceKey,
|
|
117
|
-
),
|
|
118
|
-
[commitWithFreshQueue, showToast],
|
|
119
|
-
);
|
|
112
|
+
const { stageElementPositionOffset, handleDomBoxSizeCommit, handleDomRotationCommit } =
|
|
113
|
+
useDomGeometryCommits({
|
|
114
|
+
showToast,
|
|
115
|
+
commitPositionPatchToHtml: commitWithFreshQueue,
|
|
116
|
+
readOnlyPreview: false,
|
|
117
|
+
});
|
|
120
118
|
const makeFetchFallback = useGsapAnimationFetchFallback(projectId);
|
|
121
119
|
const trackGsapInteractionFailure = useGsapInteractionFailureTelemetry(activeCompPath, showToast);
|
|
122
120
|
const {
|
|
@@ -135,7 +133,8 @@ export function useDomGeometryCommit({
|
|
|
135
133
|
makeFetchFallback,
|
|
136
134
|
trackGsapInteractionFailure,
|
|
137
135
|
stageElementPositionOffset,
|
|
138
|
-
handleDomBoxSizeCommit
|
|
136
|
+
handleDomBoxSizeCommit,
|
|
137
|
+
handleDomRotationCommit,
|
|
139
138
|
commitPositionPatchToHtml: commitWithFreshQueue,
|
|
140
139
|
addGsapAnimation: gsap.addGsapAnimation,
|
|
141
140
|
convertToKeyframes: gsap.convertToKeyframes,
|