@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
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
readStudioRotation,
|
|
14
14
|
} from "../components/editor/manualEdits";
|
|
15
15
|
import { useDomGeometryCommits, type UseDomGeometryCommitsParams } from "./useDomGeometryCommits";
|
|
16
|
+
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
17
|
+
import { writeTranslatePx } from "../components/editor/plainTranslate";
|
|
18
|
+
import { applyPatch } from "../utils/sourcePatcher";
|
|
16
19
|
import { DomEditCropHandles } from "../components/editor/DomEditCropHandles";
|
|
17
20
|
import { withInlineLayoutBox } from "./domSelectionTestHarness";
|
|
18
21
|
|
|
@@ -25,7 +28,6 @@ function mountCommits(
|
|
|
25
28
|
let commits: ReturnType<typeof useDomGeometryCommits> | null = null;
|
|
26
29
|
function Probe() {
|
|
27
30
|
commits = useDomGeometryCommits({
|
|
28
|
-
previewIframeRef: { current: null },
|
|
29
31
|
showToast: vi.fn(),
|
|
30
32
|
commitPositionPatchToHtml,
|
|
31
33
|
readOnlyPreview,
|
|
@@ -61,7 +63,6 @@ describe("useDomGeometryCommits rollback", () => {
|
|
|
61
63
|
|
|
62
64
|
function Probe() {
|
|
63
65
|
commits = useDomGeometryCommits({
|
|
64
|
-
previewIframeRef: { current: null },
|
|
65
66
|
showToast: vi.fn(),
|
|
66
67
|
commitPositionPatchToHtml,
|
|
67
68
|
readOnlyPreview: false,
|
|
@@ -70,9 +71,6 @@ describe("useDomGeometryCommits rollback", () => {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
act(() => root.render(<Probe />));
|
|
73
|
-
await expect(commits!.handleDomPathOffsetCommit(selection, { x: 50, y: 60 })).rejects.toBe(
|
|
74
|
-
failure,
|
|
75
|
-
);
|
|
76
74
|
await expect(
|
|
77
75
|
commits!.handleDomBoxSizeCommit(selection, { width: 200, height: 160 }, { x: 30, y: 40 }),
|
|
78
76
|
).rejects.toBe(failure);
|
|
@@ -90,20 +88,6 @@ describe("useDomGeometryCommits read-only preview", () => {
|
|
|
90
88
|
const selectionOn = (element: HTMLElement) =>
|
|
91
89
|
({ id: element.id, selector: `#${element.id}`, element }) as unknown as DomEditSelection;
|
|
92
90
|
|
|
93
|
-
it("refuses a manual offset commit: no write, no history entry", async () => {
|
|
94
|
-
const element = document.createElement("div");
|
|
95
|
-
element.id = "ro-offset";
|
|
96
|
-
document.body.append(element);
|
|
97
|
-
applyStudioPathOffset(element, { x: 1, y: 2 });
|
|
98
|
-
const commitPositionPatchToHtml =
|
|
99
|
-
vi.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>();
|
|
100
|
-
const { commits, unmount } = mountCommits(commitPositionPatchToHtml, true);
|
|
101
|
-
await commits().handleDomPathOffsetCommit(selectionOn(element), { x: 99, y: 99 });
|
|
102
|
-
expect(readStudioPathOffset(element)).toEqual({ x: 1, y: 2 });
|
|
103
|
-
expect(commitPositionPatchToHtml).not.toHaveBeenCalled();
|
|
104
|
-
unmount();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
91
|
it("refuses a manual box-size commit: no write, no history entry", async () => {
|
|
108
92
|
const element = document.createElement("div");
|
|
109
93
|
element.id = "ro-size";
|
|
@@ -131,19 +115,6 @@ describe("useDomGeometryCommits read-only preview", () => {
|
|
|
131
115
|
expect(commitPositionPatchToHtml).not.toHaveBeenCalled();
|
|
132
116
|
unmount();
|
|
133
117
|
});
|
|
134
|
-
|
|
135
|
-
it("still commits an offset with the flag off", async () => {
|
|
136
|
-
const element = document.createElement("div");
|
|
137
|
-
element.id = "rw-offset";
|
|
138
|
-
document.body.append(element);
|
|
139
|
-
const commitPositionPatchToHtml = vi
|
|
140
|
-
.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
|
|
141
|
-
.mockResolvedValue(undefined);
|
|
142
|
-
const { commits, unmount } = mountCommits(commitPositionPatchToHtml);
|
|
143
|
-
await commits().handleDomPathOffsetCommit(selectionOn(element), { x: 5, y: 6 });
|
|
144
|
-
expect(commitPositionPatchToHtml).toHaveBeenCalledTimes(1);
|
|
145
|
-
unmount();
|
|
146
|
-
});
|
|
147
118
|
});
|
|
148
119
|
|
|
149
120
|
describe("useDomGeometryCommits element position offset", () => {
|
|
@@ -162,7 +133,7 @@ describe("useDomGeometryCommits element position offset", () => {
|
|
|
162
133
|
.mockResolvedValue(undefined);
|
|
163
134
|
const { commits, unmount } = mountCommits(commitPositionPatchToHtml);
|
|
164
135
|
|
|
165
|
-
await commits().stageElementPositionOffset(selection, { x: 40, y: 20 }).save();
|
|
136
|
+
await commits().stageElementPositionOffset(selection, { x: 40, y: 20 }, false).save();
|
|
166
137
|
|
|
167
138
|
const patches = commitPositionPatchToHtml.mock.calls[0]![1];
|
|
168
139
|
expect(patches).toEqual([
|
|
@@ -175,6 +146,235 @@ describe("useDomGeometryCommits element position offset", () => {
|
|
|
175
146
|
});
|
|
176
147
|
});
|
|
177
148
|
|
|
149
|
+
describe("useDomGeometryCommits resize anchor", () => {
|
|
150
|
+
it("saves the anchor as the element's own plain px translate, sub-pixel, with the size", async () => {
|
|
151
|
+
const element = document.createElement("div");
|
|
152
|
+
element.id = "anchored";
|
|
153
|
+
element.style.setProperty("translate", "40px 30px");
|
|
154
|
+
document.body.append(element);
|
|
155
|
+
const selection = {
|
|
156
|
+
id: "anchored",
|
|
157
|
+
selector: "#anchored",
|
|
158
|
+
element,
|
|
159
|
+
} as unknown as DomEditSelection;
|
|
160
|
+
const commit = vi
|
|
161
|
+
.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
|
|
162
|
+
.mockResolvedValue(undefined);
|
|
163
|
+
const { commits, unmount } = mountCommits(commit);
|
|
164
|
+
|
|
165
|
+
await commits().handleDomBoxSizeCommit(
|
|
166
|
+
selection,
|
|
167
|
+
{ width: 340, height: 227 },
|
|
168
|
+
{ x: -10.25, y: 3.5 },
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(commit).toHaveBeenCalledTimes(1);
|
|
172
|
+
const patches = commit.mock.calls[0]![1];
|
|
173
|
+
expect(patches).toContainEqual({ type: "inline-style", property: "width", value: "340px" });
|
|
174
|
+
expect(patches).toContainEqual({
|
|
175
|
+
type: "inline-style",
|
|
176
|
+
property: "translate",
|
|
177
|
+
value: "-10.25px 3.5px",
|
|
178
|
+
});
|
|
179
|
+
expect(patches.some((p) => p.property.startsWith("--hf-studio-offset"))).toBe(false);
|
|
180
|
+
expect(element.style.getPropertyValue("translate")).toBe("-10.25px 3.5px");
|
|
181
|
+
unmount();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("useDomGeometryCommits resize rollback", () => {
|
|
186
|
+
it("rolls the whole gesture back once when the resize save fails", async () => {
|
|
187
|
+
const element = document.createElement("div");
|
|
188
|
+
document.body.append(element);
|
|
189
|
+
const selection = { id: "r", selector: "#r", element } as unknown as DomEditSelection;
|
|
190
|
+
const failure = new Error("save failed");
|
|
191
|
+
const commit = vi
|
|
192
|
+
.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
|
|
193
|
+
.mockRejectedValue(failure);
|
|
194
|
+
const restore = vi.fn();
|
|
195
|
+
const { commits, unmount } = mountCommits(commit);
|
|
196
|
+
await expect(
|
|
197
|
+
commits().handleDomBoxSizeCommit(selection, { width: 50, height: 40 }, undefined, restore),
|
|
198
|
+
).rejects.toBe(failure);
|
|
199
|
+
expect(restore).toHaveBeenCalledTimes(1);
|
|
200
|
+
unmount();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("a plain move of an element an older Studio moved by its offset vars", () => {
|
|
205
|
+
function legacyMoved() {
|
|
206
|
+
const element = document.createElement("div");
|
|
207
|
+
element.id = "legacy";
|
|
208
|
+
element.style.setProperty("translate", "10px 5px");
|
|
209
|
+
document.body.append(element);
|
|
210
|
+
applyStudioPathOffset(element, { x: 40, y: 30 });
|
|
211
|
+
const selection = { id: "legacy", selector: "#legacy", element } as unknown as DomEditSelection;
|
|
212
|
+
return { element, selection };
|
|
213
|
+
}
|
|
214
|
+
const legacyEnds = [
|
|
215
|
+
{ type: "inline-style", property: "--hf-studio-offset-x", value: null },
|
|
216
|
+
{ type: "inline-style", property: "--hf-studio-offset-y", value: null },
|
|
217
|
+
{ type: "attribute", property: "data-hf-studio-path-offset", value: null },
|
|
218
|
+
];
|
|
219
|
+
const keptMarks = [
|
|
220
|
+
"data-hf-studio-original-inline-translate",
|
|
221
|
+
"data-hf-studio-original-translate",
|
|
222
|
+
];
|
|
223
|
+
const studioMarks = (el: HTMLElement) =>
|
|
224
|
+
el
|
|
225
|
+
.getAttributeNames()
|
|
226
|
+
.filter((name) => name.startsWith("data-hf-studio"))
|
|
227
|
+
.sort();
|
|
228
|
+
|
|
229
|
+
it("saves the move as its translate, ends the old offset, and a seek keeps it", async () => {
|
|
230
|
+
const { element, selection } = legacyMoved();
|
|
231
|
+
const commitPositionPatchToHtml = vi
|
|
232
|
+
.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
|
|
233
|
+
.mockResolvedValue(undefined);
|
|
234
|
+
const { commits, unmount } = mountCommits(commitPositionPatchToHtml);
|
|
235
|
+
|
|
236
|
+
await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
|
|
237
|
+
reapplyPositionEditsAfterSeek(document);
|
|
238
|
+
|
|
239
|
+
expect(element.style.getPropertyValue("translate")).toBe("140px 30px");
|
|
240
|
+
expect(commitPositionPatchToHtml.mock.calls[0]![1]).toEqual([
|
|
241
|
+
{ type: "inline-style", property: "translate", value: "140px 30px" },
|
|
242
|
+
{
|
|
243
|
+
type: "attribute",
|
|
244
|
+
property: "data-hf-studio-original-inline-translate",
|
|
245
|
+
value: "10px 5px",
|
|
246
|
+
},
|
|
247
|
+
...legacyEnds,
|
|
248
|
+
]);
|
|
249
|
+
expect(studioMarks(element)).toEqual(keptMarks);
|
|
250
|
+
unmount();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("resizes with the anchor as its translate, ends the old offset, and a seek keeps it", async () => {
|
|
254
|
+
const { element, selection } = legacyMoved();
|
|
255
|
+
const commit = vi
|
|
256
|
+
.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
|
|
257
|
+
.mockResolvedValue(undefined);
|
|
258
|
+
const { commits, unmount } = mountCommits(commit);
|
|
259
|
+
|
|
260
|
+
await commits().handleDomBoxSizeCommit(selection, { width: 80, height: 60 }, { x: 60, y: 40 });
|
|
261
|
+
reapplyPositionEditsAfterSeek(document);
|
|
262
|
+
|
|
263
|
+
expect(element.style.getPropertyValue("translate")).toBe("60px 40px");
|
|
264
|
+
expect(commit.mock.calls[0]![1]).toEqual(expect.arrayContaining(legacyEnds));
|
|
265
|
+
expect(studioMarks(element)).toEqual(expect.arrayContaining(keptMarks));
|
|
266
|
+
expect(element.hasAttribute("data-hf-studio-path-offset")).toBe(false);
|
|
267
|
+
unmount();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it.each(["move", "resize"])(
|
|
271
|
+
"puts the old offset back whole when the %s save fails",
|
|
272
|
+
async (kind) => {
|
|
273
|
+
const { element, selection } = legacyMoved();
|
|
274
|
+
const translate = element.style.getPropertyValue("translate");
|
|
275
|
+
const { commits, unmount } = mountCommits(
|
|
276
|
+
vi.fn().mockRejectedValue(new Error("save failed")),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const save =
|
|
280
|
+
kind === "move"
|
|
281
|
+
? commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save()
|
|
282
|
+
: commits().handleDomBoxSizeCommit(
|
|
283
|
+
selection,
|
|
284
|
+
{ width: 80, height: 60 },
|
|
285
|
+
{ x: 60, y: 40 },
|
|
286
|
+
);
|
|
287
|
+
await expect(save).rejects.toThrow();
|
|
288
|
+
expect(element.style.getPropertyValue("translate")).toBe(translate);
|
|
289
|
+
expect(element.hasAttribute("data-hf-studio-path-offset")).toBe(true);
|
|
290
|
+
expect(readStudioPathOffset(element)).toEqual({ x: 40, y: 30 });
|
|
291
|
+
unmount();
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("Reset after a plain move puts the author's translate back", () => {
|
|
297
|
+
function authoredLayer(translate: string | null, legacy = false) {
|
|
298
|
+
const element = document.createElement("div");
|
|
299
|
+
element.id = "layer";
|
|
300
|
+
if (translate) element.style.setProperty("translate", translate);
|
|
301
|
+
document.body.append(element);
|
|
302
|
+
if (legacy) applyStudioPathOffset(element, { x: 40, y: 30 });
|
|
303
|
+
const project = { file: element.outerHTML, history: [] as string[] };
|
|
304
|
+
const commit = vi.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>(
|
|
305
|
+
async (_selection, patches) => {
|
|
306
|
+
project.history.push(project.file);
|
|
307
|
+
project.file = patches.reduce((html, op) => applyPatch(html, "layer", op), project.file);
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
const selection = { id: "layer", selector: "#layer", element } as unknown as DomEditSelection;
|
|
311
|
+
return { element, selection, project, ...mountCommits(commit) };
|
|
312
|
+
}
|
|
313
|
+
const saved = (html: string) =>
|
|
314
|
+
new DOMParser().parseFromString(html, "text/html").getElementById("layer")!;
|
|
315
|
+
const studioLeftovers = (el: Element) => [
|
|
316
|
+
...el.getAttributeNames().filter((name) => name.startsWith("data-hf-studio")),
|
|
317
|
+
...((el as HTMLElement).style.cssText.match(/--hf-studio-[\w-]+/g) ?? []),
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
it("restores an older Studio's layer to its authored 50% 10px, in the file and live", async () => {
|
|
321
|
+
const { element, selection, project, commits, unmount } = authoredLayer("50% 10px", true);
|
|
322
|
+
|
|
323
|
+
await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
|
|
324
|
+
await commits().handleDomManualEditsReset(selection);
|
|
325
|
+
|
|
326
|
+
expect(saved(project.file).style.getPropertyValue("translate")).toBe("50% 10px");
|
|
327
|
+
expect(element.style.getPropertyValue("translate")).toBe("50% 10px");
|
|
328
|
+
expect(studioLeftovers(saved(project.file))).toEqual([]);
|
|
329
|
+
expect(studioLeftovers(element)).toEqual([]);
|
|
330
|
+
unmount();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("restores the authored 12px 4px after a drag and a second move", async () => {
|
|
334
|
+
const { element, selection, project, commits, unmount } = authoredLayer("12px 4px");
|
|
335
|
+
|
|
336
|
+
writeTranslatePx(element, { x: 90, y: 0 });
|
|
337
|
+
await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
|
|
338
|
+
await commits().stageElementPositionOffset(selection, { x: 200, y: 50 }, true).save();
|
|
339
|
+
await commits().handleDomManualEditsReset(selection);
|
|
340
|
+
|
|
341
|
+
expect(saved(project.file).style.getPropertyValue("translate")).toBe("12px 4px");
|
|
342
|
+
expect(element.style.getPropertyValue("translate")).toBe("12px 4px");
|
|
343
|
+
expect(studioLeftovers(saved(project.file))).toEqual([]);
|
|
344
|
+
unmount();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("removes the translate of a layer that had none, in the file and live", async () => {
|
|
348
|
+
const { element, selection, project, commits, unmount } = authoredLayer(null);
|
|
349
|
+
|
|
350
|
+
await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
|
|
351
|
+
await commits().handleDomManualEditsReset(selection);
|
|
352
|
+
|
|
353
|
+
expect(saved(project.file).style.getPropertyValue("translate")).toBe("");
|
|
354
|
+
expect(element.style.getPropertyValue("translate")).toBe("");
|
|
355
|
+
expect(studioLeftovers(saved(project.file))).toEqual([]);
|
|
356
|
+
unmount();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("undo of the Reset brings the move back, and Reset still restores the author's", async () => {
|
|
360
|
+
const { selection, project, commits, unmount } = authoredLayer("12px 4px");
|
|
361
|
+
|
|
362
|
+
await commits().stageElementPositionOffset(selection, { x: 140, y: 30 }, true).save();
|
|
363
|
+
const moved = project.file;
|
|
364
|
+
await commits().handleDomManualEditsReset(selection);
|
|
365
|
+
project.file = project.history.pop()!;
|
|
366
|
+
expect(project.file).toBe(moved);
|
|
367
|
+
|
|
368
|
+
const reloaded = saved(project.file) as HTMLElement;
|
|
369
|
+
document.body.append(reloaded);
|
|
370
|
+
expect(reloaded.style.getPropertyValue("translate")).toBe("140px 30px");
|
|
371
|
+
await commits().handleDomManualEditsReset({ ...selection, element: reloaded });
|
|
372
|
+
expect(saved(project.file).style.getPropertyValue("translate")).toBe("12px 4px");
|
|
373
|
+
expect(reloaded.style.getPropertyValue("translate")).toBe("12px 4px");
|
|
374
|
+
unmount();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
178
378
|
describe("useDomGeometryCommits resize of a cropped element", () => {
|
|
179
379
|
it("saves the crop scaled per axis with the box in the resize's own commit", async () => {
|
|
180
380
|
const element = withInlineLayoutBox(document.createElement("div"));
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
1
2
|
import { useCallback } from "react";
|
|
2
3
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
3
4
|
import {
|
|
4
|
-
applyStudioPathOffset,
|
|
5
5
|
applyStudioBoxSize,
|
|
6
|
-
applyStudioRotation,
|
|
7
6
|
captureStudioPathOffset,
|
|
8
7
|
captureStudioBoxSize,
|
|
9
8
|
captureStudioRotation,
|
|
@@ -15,36 +14,20 @@ import {
|
|
|
15
14
|
clearStudioRotation,
|
|
16
15
|
} from "../components/editor/manualEdits";
|
|
17
16
|
import { stageElementOffset } from "./elementOffsetStager";
|
|
17
|
+
import { savePlainRotation } from "./plainRotation";
|
|
18
18
|
import { prepareCropResize } from "../components/editor/cropResize";
|
|
19
|
+
import { writePlainMove } from "../components/editor/plainTranslate";
|
|
19
20
|
import {
|
|
20
|
-
buildPathOffsetPatches,
|
|
21
21
|
buildBoxSizePatches,
|
|
22
|
-
buildRotationPatches,
|
|
23
22
|
buildClearPathOffsetPatches,
|
|
24
23
|
buildClearBoxSizePatches,
|
|
25
24
|
buildClearRotationPatches,
|
|
26
25
|
} from "../components/editor/manualEditsDomPatches";
|
|
27
26
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
28
|
-
import { isElementGsapTargeted } from "./gsapTargetCache";
|
|
29
|
-
|
|
30
|
-
const GSAP_CSS_FALLBACK_BLOCKED_MESSAGE =
|
|
31
|
-
"This element is GSAP-animated — dragging via CSS would corrupt keyframes";
|
|
32
|
-
|
|
33
|
-
function rejectGsapCssFallback(
|
|
34
|
-
selection: DomEditSelection,
|
|
35
|
-
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>,
|
|
36
|
-
showToast: (message: string, tone?: "error" | "info") => void,
|
|
37
|
-
): Promise<never> | null {
|
|
38
|
-
if (!isElementGsapTargeted(previewIframeRef.current, selection.element)) return null;
|
|
39
|
-
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
40
|
-
showToast(error.message, "error");
|
|
41
|
-
return Promise.reject(error);
|
|
42
|
-
}
|
|
43
27
|
|
|
44
28
|
// ── Hook ──
|
|
45
29
|
|
|
46
30
|
export interface UseDomGeometryCommitsParams {
|
|
47
|
-
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
48
31
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
49
32
|
commitPositionPatchToHtml: (
|
|
50
33
|
selection: DomEditSelection,
|
|
@@ -55,99 +38,64 @@ export interface UseDomGeometryCommitsParams {
|
|
|
55
38
|
}
|
|
56
39
|
|
|
57
40
|
export function useDomGeometryCommits({
|
|
58
|
-
previewIframeRef,
|
|
59
41
|
showToast,
|
|
60
42
|
commitPositionPatchToHtml,
|
|
61
43
|
readOnlyPreview,
|
|
62
44
|
}: UseDomGeometryCommitsParams) {
|
|
63
45
|
const stageElementPositionOffset = useCallback(
|
|
64
|
-
(
|
|
46
|
+
(
|
|
47
|
+
selection: DomEditSelection,
|
|
48
|
+
next: { x: number; y: number },
|
|
49
|
+
plainTranslate: boolean,
|
|
50
|
+
coalesceKey?: string,
|
|
51
|
+
) =>
|
|
65
52
|
stageElementOffset(
|
|
66
53
|
{ commitPositionPatchToHtml, showToast, readOnlyPreview },
|
|
67
54
|
selection,
|
|
68
55
|
next,
|
|
56
|
+
plainTranslate,
|
|
69
57
|
coalesceKey,
|
|
70
58
|
),
|
|
71
59
|
[commitPositionPatchToHtml, readOnlyPreview, showToast],
|
|
72
60
|
);
|
|
73
61
|
|
|
74
|
-
const handleDomPathOffsetCommit = useCallback(
|
|
75
|
-
(selection: DomEditSelection, next: { x: number; y: number }) => {
|
|
76
|
-
if (readOnlyPreview) return Promise.resolve();
|
|
77
|
-
// ponytail: GSAP-targeted elements are blocked (no SDK position-in-script op); CSS-path
|
|
78
|
-
// elements fall through to commitPositionPatchToHtml → persistDomEditOperations →
|
|
79
|
-
// onTrySdkPersist and are already SDK-cut-over as setStyle/setAttribute (§3.3 done).
|
|
80
|
-
// Upgrade path for GSAP: add a moveElementGsap SDK op in a separate SDK PR.
|
|
81
|
-
const gsapFallback = rejectGsapCssFallback(selection, previewIframeRef, showToast);
|
|
82
|
-
if (gsapFallback) return gsapFallback;
|
|
83
|
-
const before = captureStudioPathOffset(selection.element);
|
|
84
|
-
applyStudioPathOffset(selection.element, next);
|
|
85
|
-
return commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
|
|
86
|
-
label: "Move layer",
|
|
87
|
-
coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
|
|
88
|
-
}).catch((error) => {
|
|
89
|
-
restoreStudioPathOffset(selection.element, before);
|
|
90
|
-
throw error;
|
|
91
|
-
});
|
|
92
|
-
},
|
|
93
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast, readOnlyPreview],
|
|
94
|
-
);
|
|
95
|
-
|
|
96
62
|
const handleDomBoxSizeCommit = useCallback(
|
|
97
63
|
(
|
|
98
64
|
selection: DomEditSelection,
|
|
99
65
|
next: { width: number; height: number },
|
|
100
66
|
offset?: { x: number; y: number },
|
|
67
|
+
restore?: () => void,
|
|
101
68
|
) => {
|
|
102
69
|
if (readOnlyPreview) return Promise.resolve();
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
applyStudioBoxSize(selection.element, next);
|
|
70
|
+
const element = selection.element;
|
|
71
|
+
const beforeSize = captureStudioBoxSize(element);
|
|
72
|
+
const beforeOffset = captureStudioPathOffset(element);
|
|
73
|
+
const stageCrop = prepareCropResize(element);
|
|
74
|
+
applyStudioBoxSize(element, next);
|
|
109
75
|
const crop = stageCrop();
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
// intermediate re-stamp where the new size is in source but the anchor
|
|
114
|
-
// offset is not (that frame was the release "jump"). Both builders read
|
|
115
|
-
// the already-mutated live element, so concatenation is safe.
|
|
116
|
-
const patches = buildBoxSizePatches(selection.element);
|
|
76
|
+
// One commit, one undo entry: the size, the crop that follows it, and the translate
|
|
77
|
+
// (as a move writes it) that keeps the centre planted.
|
|
78
|
+
const patches = buildBoxSizePatches(element);
|
|
117
79
|
if (crop) patches.push(crop.patch);
|
|
118
|
-
if (offset)
|
|
119
|
-
applyStudioPathOffset(selection.element, offset);
|
|
120
|
-
patches.push(...buildPathOffsetPatches(selection.element));
|
|
121
|
-
}
|
|
80
|
+
if (offset) patches.push(...writePlainMove(element, offset));
|
|
122
81
|
return commitPositionPatchToHtml(selection, patches, {
|
|
123
82
|
label: "Resize layer box",
|
|
124
83
|
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
|
125
84
|
}).catch((error) => {
|
|
126
|
-
restoreStudioBoxSize(
|
|
127
|
-
if (
|
|
85
|
+
restoreStudioBoxSize(element, beforeSize);
|
|
86
|
+
if (offset) restoreStudioPathOffset(element, beforeOffset);
|
|
128
87
|
crop?.revert();
|
|
88
|
+
restore?.();
|
|
129
89
|
throw error;
|
|
130
90
|
});
|
|
131
91
|
},
|
|
132
|
-
[commitPositionPatchToHtml,
|
|
92
|
+
[commitPositionPatchToHtml, readOnlyPreview],
|
|
133
93
|
);
|
|
134
94
|
|
|
135
95
|
const handleDomRotationCommit = useCallback(
|
|
136
|
-
(selection: DomEditSelection, next:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (gsapFallback) return gsapFallback;
|
|
140
|
-
const before = captureStudioRotation(selection.element);
|
|
141
|
-
applyStudioRotation(selection.element, next);
|
|
142
|
-
return commitPositionPatchToHtml(selection, buildRotationPatches(selection.element), {
|
|
143
|
-
label: "Rotate layer",
|
|
144
|
-
coalesceKey: `rotation:${getDomEditTargetKey(selection)}`,
|
|
145
|
-
}).catch((error) => {
|
|
146
|
-
restoreStudioRotation(selection.element, before);
|
|
147
|
-
throw error;
|
|
148
|
-
});
|
|
149
|
-
},
|
|
150
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast, readOnlyPreview],
|
|
96
|
+
(selection: DomEditSelection, next: RotationCommit) =>
|
|
97
|
+
savePlainRotation({ commitPositionPatchToHtml, readOnlyPreview }, selection, next),
|
|
98
|
+
[commitPositionPatchToHtml, readOnlyPreview],
|
|
151
99
|
);
|
|
152
100
|
|
|
153
101
|
const handleDomManualEditsReset = useCallback(
|
|
@@ -181,7 +129,6 @@ export function useDomGeometryCommits({
|
|
|
181
129
|
|
|
182
130
|
return {
|
|
183
131
|
stageElementPositionOffset,
|
|
184
|
-
handleDomPathOffsetCommit,
|
|
185
132
|
handleDomBoxSizeCommit,
|
|
186
133
|
handleDomRotationCommit,
|
|
187
134
|
handleDomManualEditsReset,
|