@hyperframes/studio 0.8.104 → 0.8.106
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-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/index-CMiYVGv5.js +399 -0
- package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +10391 -9836
- 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 +9 -9
- 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/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- 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 +35 -13
- package/src/components/StudioHeader.tsx +6 -2
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/ThemeToggle.tsx +99 -0
- 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 +37 -5
- 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/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/SourceEditor.tsx +2 -6
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +36 -3
- package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
- package/src/components/editor/manualEditsDomPatches.ts +0 -30
- package/src/components/editor/manualEditsSnapshot.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +34 -3
- 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.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +71 -48
- 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/propertyPanelHelpers.ts +1 -2
- 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/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- 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/appHotkeysDispatch.ts +2 -1
- package/src/hooks/elementOffsetStager.ts +8 -7
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +141 -23
- package/src/hooks/useClipboard.ts +154 -123
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
- package/src/hooks/useDomGeometryCommit.ts +0 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
- package/src/hooks/useDomGeometryCommits.ts +4 -49
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- 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.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +16 -8
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- 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/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- 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/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +4 -7
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +214 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +12 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/assets/index-De2aO4XM.js +0 -399
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/dist/chunk-HIGHVWYD.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
|
@@ -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", () => {
|
|
@@ -230,6 +201,180 @@ describe("useDomGeometryCommits resize rollback", () => {
|
|
|
230
201
|
});
|
|
231
202
|
});
|
|
232
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
|
+
|
|
233
378
|
describe("useDomGeometryCommits resize of a cropped element", () => {
|
|
234
379
|
it("saves the crop scaled per axis with the box in the resize's own commit", async () => {
|
|
235
380
|
const element = withInlineLayoutBox(document.createElement("div"));
|
|
@@ -2,7 +2,6 @@ import type { RotationCommit } from "../components/editor/rotationDraft";
|
|
|
2
2
|
import { useCallback } from "react";
|
|
3
3
|
import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing";
|
|
4
4
|
import {
|
|
5
|
-
applyStudioPathOffset,
|
|
6
5
|
applyStudioBoxSize,
|
|
7
6
|
captureStudioPathOffset,
|
|
8
7
|
captureStudioBoxSize,
|
|
@@ -17,35 +16,18 @@ import {
|
|
|
17
16
|
import { stageElementOffset } from "./elementOffsetStager";
|
|
18
17
|
import { savePlainRotation } from "./plainRotation";
|
|
19
18
|
import { prepareCropResize } from "../components/editor/cropResize";
|
|
20
|
-
import {
|
|
19
|
+
import { writePlainMove } from "../components/editor/plainTranslate";
|
|
21
20
|
import {
|
|
22
|
-
buildPathOffsetPatches,
|
|
23
21
|
buildBoxSizePatches,
|
|
24
22
|
buildClearPathOffsetPatches,
|
|
25
23
|
buildClearBoxSizePatches,
|
|
26
24
|
buildClearRotationPatches,
|
|
27
25
|
} from "../components/editor/manualEditsDomPatches";
|
|
28
26
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
29
|
-
import { isElementGsapTargeted } from "./gsapTargetCache";
|
|
30
|
-
|
|
31
|
-
const GSAP_CSS_FALLBACK_BLOCKED_MESSAGE =
|
|
32
|
-
"This element is GSAP-animated — dragging via CSS would corrupt keyframes";
|
|
33
|
-
|
|
34
|
-
function rejectGsapCssFallback(
|
|
35
|
-
selection: DomEditSelection,
|
|
36
|
-
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>,
|
|
37
|
-
showToast: (message: string, tone?: "error" | "info") => void,
|
|
38
|
-
): Promise<never> | null {
|
|
39
|
-
if (!isElementGsapTargeted(previewIframeRef.current, selection.element)) return null;
|
|
40
|
-
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
41
|
-
showToast(error.message, "error");
|
|
42
|
-
return Promise.reject(error);
|
|
43
|
-
}
|
|
44
27
|
|
|
45
28
|
// ── Hook ──
|
|
46
29
|
|
|
47
30
|
export interface UseDomGeometryCommitsParams {
|
|
48
|
-
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
49
31
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
50
32
|
commitPositionPatchToHtml: (
|
|
51
33
|
selection: DomEditSelection,
|
|
@@ -56,7 +38,6 @@ export interface UseDomGeometryCommitsParams {
|
|
|
56
38
|
}
|
|
57
39
|
|
|
58
40
|
export function useDomGeometryCommits({
|
|
59
|
-
previewIframeRef,
|
|
60
41
|
showToast,
|
|
61
42
|
commitPositionPatchToHtml,
|
|
62
43
|
readOnlyPreview,
|
|
@@ -78,28 +59,6 @@ export function useDomGeometryCommits({
|
|
|
78
59
|
[commitPositionPatchToHtml, readOnlyPreview, showToast],
|
|
79
60
|
);
|
|
80
61
|
|
|
81
|
-
const handleDomPathOffsetCommit = useCallback(
|
|
82
|
-
(selection: DomEditSelection, next: { x: number; y: number }) => {
|
|
83
|
-
if (readOnlyPreview) return Promise.resolve();
|
|
84
|
-
// ponytail: GSAP-targeted elements are blocked (no SDK position-in-script op); CSS-path
|
|
85
|
-
// elements fall through to commitPositionPatchToHtml → persistDomEditOperations →
|
|
86
|
-
// onTrySdkPersist and are already SDK-cut-over as setStyle/setAttribute (§3.3 done).
|
|
87
|
-
// Upgrade path for GSAP: add a moveElementGsap SDK op in a separate SDK PR.
|
|
88
|
-
const gsapFallback = rejectGsapCssFallback(selection, previewIframeRef, showToast);
|
|
89
|
-
if (gsapFallback) return gsapFallback;
|
|
90
|
-
const before = captureStudioPathOffset(selection.element);
|
|
91
|
-
applyStudioPathOffset(selection.element, next);
|
|
92
|
-
return commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
|
|
93
|
-
label: "Move layer",
|
|
94
|
-
coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`,
|
|
95
|
-
}).catch((error) => {
|
|
96
|
-
restoreStudioPathOffset(selection.element, before);
|
|
97
|
-
throw error;
|
|
98
|
-
});
|
|
99
|
-
},
|
|
100
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast, readOnlyPreview],
|
|
101
|
-
);
|
|
102
|
-
|
|
103
62
|
const handleDomBoxSizeCommit = useCallback(
|
|
104
63
|
(
|
|
105
64
|
selection: DomEditSelection,
|
|
@@ -110,7 +69,7 @@ export function useDomGeometryCommits({
|
|
|
110
69
|
if (readOnlyPreview) return Promise.resolve();
|
|
111
70
|
const element = selection.element;
|
|
112
71
|
const beforeSize = captureStudioBoxSize(element);
|
|
113
|
-
const
|
|
72
|
+
const beforeOffset = captureStudioPathOffset(element);
|
|
114
73
|
const stageCrop = prepareCropResize(element);
|
|
115
74
|
applyStudioBoxSize(element, next);
|
|
116
75
|
const crop = stageCrop();
|
|
@@ -118,16 +77,13 @@ export function useDomGeometryCommits({
|
|
|
118
77
|
// (as a move writes it) that keeps the centre planted.
|
|
119
78
|
const patches = buildBoxSizePatches(element);
|
|
120
79
|
if (crop) patches.push(crop.patch);
|
|
121
|
-
if (offset)
|
|
122
|
-
writeTranslatePx(element, offset);
|
|
123
|
-
patches.push(translatePatch(offset));
|
|
124
|
-
}
|
|
80
|
+
if (offset) patches.push(...writePlainMove(element, offset));
|
|
125
81
|
return commitPositionPatchToHtml(selection, patches, {
|
|
126
82
|
label: "Resize layer box",
|
|
127
83
|
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
|
128
84
|
}).catch((error) => {
|
|
129
85
|
restoreStudioBoxSize(element, beforeSize);
|
|
130
|
-
if (offset) element
|
|
86
|
+
if (offset) restoreStudioPathOffset(element, beforeOffset);
|
|
131
87
|
crop?.revert();
|
|
132
88
|
restore?.();
|
|
133
89
|
throw error;
|
|
@@ -173,7 +129,6 @@ export function useDomGeometryCommits({
|
|
|
173
129
|
|
|
174
130
|
return {
|
|
175
131
|
stageElementPositionOffset,
|
|
176
|
-
handleDomPathOffsetCommit,
|
|
177
132
|
handleDomBoxSizeCommit,
|
|
178
133
|
handleDomRotationCommit,
|
|
179
134
|
handleDomManualEditsReset,
|
|
@@ -336,8 +336,7 @@ export function usePopulateKeyframeCacheForFile(
|
|
|
336
336
|
// fallow-ignore-next-line complexity
|
|
337
337
|
const tryRuntimeScan = () => {
|
|
338
338
|
if (runtimeScanDoneRef.current === `kf-cache:${projectId}:${sf}:${version}`) return true;
|
|
339
|
-
const iframe =
|
|
340
|
-
iframeRef?.current ?? document.querySelector<HTMLIFrameElement>("iframe[src*='/preview/']");
|
|
339
|
+
const iframe = iframeRef?.current;
|
|
341
340
|
if (!iframe) return false;
|
|
342
341
|
// Clip dims per element so the scan converts tween-relative keyframes to
|
|
343
342
|
// clip-relative (matching the static path) instead of timeline-relative.
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { sanitizeRichTextChildren } from "@hyperframes/core/rich-text-sanitize";
|
|
3
3
|
import { usePreviewReadOnly } from "../components/editor/previewReadOnlyContext";
|
|
4
|
+
import { trackStudioPendingEdit } from "../utils/studioPendingEdits";
|
|
5
|
+
import {
|
|
6
|
+
beginStudioManualEditGesture,
|
|
7
|
+
endStudioManualEditGesture,
|
|
8
|
+
noteStudioManualEditSave,
|
|
9
|
+
} from "../components/editor/manualEditsDom";
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Editing an element's text where it sits, in the composition itself.
|
|
@@ -43,6 +49,8 @@ export interface InlineTextEditSession {
|
|
|
43
49
|
outline: string;
|
|
44
50
|
/** The element's own outline offset, restored with the outline. */
|
|
45
51
|
outlineOffset: string;
|
|
52
|
+
/** Holds the preview on screen while the edit is open. */
|
|
53
|
+
gesture?: string;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
/** The live markup to persist and the session snapshot to restore on failure. */
|
|
@@ -86,7 +94,7 @@ export function useInlineTextEdit({
|
|
|
86
94
|
onPause,
|
|
87
95
|
}: {
|
|
88
96
|
/** Where the edited text goes. The caller owns persistence. */
|
|
89
|
-
onCommit: (commit: InlineTextEditCommit) =>
|
|
97
|
+
onCommit: (commit: InlineTextEditCommit) => unknown;
|
|
90
98
|
/** Stop playback, so the element is not animating under the caret. */
|
|
91
99
|
onPause?: () => void;
|
|
92
100
|
}): InlineTextEditControls {
|
|
@@ -106,6 +114,7 @@ export function useInlineTextEdit({
|
|
|
106
114
|
framesRef.current = null;
|
|
107
115
|
}
|
|
108
116
|
openRef.current = null;
|
|
117
|
+
endStudioManualEditGesture(open.element, open.gesture);
|
|
109
118
|
setSession(null);
|
|
110
119
|
// An element removed from the document mid-session is not an error, it is
|
|
111
120
|
// just nothing left to clean up.
|
|
@@ -132,6 +141,7 @@ export function useInlineTextEdit({
|
|
|
132
141
|
original: element.innerHTML,
|
|
133
142
|
outline: element.style.outline,
|
|
134
143
|
outlineOffset: element.style.outlineOffset,
|
|
144
|
+
gesture: beginStudioManualEditGesture(element),
|
|
135
145
|
};
|
|
136
146
|
// Drawn on the element itself, not in Studio's overlay above it. This is
|
|
137
147
|
// the only mark that says the caret is in the TEXT rather than the
|
|
@@ -168,10 +178,14 @@ export function useInlineTextEdit({
|
|
|
168
178
|
// saved rather than something the server will quietly cut down.
|
|
169
179
|
sanitizeRichTextChildren(open.element);
|
|
170
180
|
const html = open.element.innerHTML;
|
|
181
|
+
// Counted and pending before the edit closes, so a reload waits for the save, not the mark.
|
|
182
|
+
let save: (saving: unknown) => void = () => {};
|
|
183
|
+
trackStudioPendingEdit(new Promise((resolve) => (save = resolve)));
|
|
184
|
+
noteStudioManualEditSave(open.element);
|
|
171
185
|
teardown();
|
|
172
186
|
// After teardown, so the commit path's own resync does not fight an
|
|
173
187
|
// element that is still editable.
|
|
174
|
-
onCommit({ element: open.element, html, previousHtml: open.original });
|
|
188
|
+
save(onCommit({ element: open.element, html, previousHtml: open.original }));
|
|
175
189
|
}, [onCommit, teardown]);
|
|
176
190
|
|
|
177
191
|
const cancel = useCallback(() => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { onPreviewPromoted } from "../player/sceneSwap";
|
|
3
|
+
|
|
4
|
+
/** The one source for which preview is on screen: the host's iframe, then each reload promoted in its place. */
|
|
5
|
+
export function useLivePreviewIframe(host: HTMLIFrameElement | null): HTMLIFrameElement | null {
|
|
6
|
+
const [promoted, setPromoted] = useState<{ host: HTMLIFrameElement; live: HTMLIFrameElement }>();
|
|
7
|
+
const live = host && promoted?.host === host ? promoted.live : host;
|
|
8
|
+
const liveRef = useRef(live);
|
|
9
|
+
liveRef.current = live;
|
|
10
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!host) return;
|
|
13
|
+
return onPreviewPromoted(host.ownerDocument, ({ retired, live: next }) => {
|
|
14
|
+
if (retired === liveRef.current) setPromoted({ host, live: next });
|
|
15
|
+
});
|
|
16
|
+
}, [host]);
|
|
17
|
+
return live;
|
|
18
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { usePlayerStore } from "../player/store/playerStore";
|
|
6
|
+
import type { TimelineElement } from "../player/store/timelineElement";
|
|
7
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
8
|
+
import { usePreviewPersistence } from "./usePreviewPersistence";
|
|
9
|
+
|
|
10
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
document.body.innerHTML = "";
|
|
14
|
+
usePlayerStore.getState().reset();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("undo that reloads the preview", () => {
|
|
18
|
+
it("keeps the timeline's clips until the reloaded preview reports its own", async () => {
|
|
19
|
+
const clips = [{ id: "a" }, { id: "b" }] as unknown as TimelineElement[];
|
|
20
|
+
usePlayerStore.getState().setElements(clips);
|
|
21
|
+
usePlayerStore.getState().setTimelineReady(true);
|
|
22
|
+
const reloadPreview = vi.fn();
|
|
23
|
+
let sync: ReturnType<typeof usePreviewPersistence>["syncHistoryPreviewAfterApply"] | null =
|
|
24
|
+
null;
|
|
25
|
+
function Harness() {
|
|
26
|
+
sync = usePreviewPersistence({
|
|
27
|
+
showToast: () => {},
|
|
28
|
+
readOptionalProjectFile: async () => "",
|
|
29
|
+
writeProjectFile: async () => {},
|
|
30
|
+
recordEdit: async () => {},
|
|
31
|
+
previewIframeRef: { current: null },
|
|
32
|
+
activeCompPathRef: { current: "index.html" },
|
|
33
|
+
reloadPreview,
|
|
34
|
+
}).syncHistoryPreviewAfterApply;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
mountReactHarness(<Harness />);
|
|
38
|
+
|
|
39
|
+
// A nested composition's undo is not the active file's, so it reloads the preview.
|
|
40
|
+
const files = { "compositions/sub.html": { previous: "<p>1</p>", restored: "<p>2</p>" } };
|
|
41
|
+
await act(async () => sync!({ paths: Object.keys(files), files }));
|
|
42
|
+
|
|
43
|
+
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
44
|
+
const player = usePlayerStore.getState();
|
|
45
|
+
expect([player.elements, player.timelineReady]).toEqual([clips, true]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -207,16 +207,13 @@ export function usePreviewPersistence({
|
|
|
207
207
|
reloadPreview,
|
|
208
208
|
);
|
|
209
209
|
if (strategy === "full") {
|
|
210
|
-
|
|
211
|
-
player.setElements([]);
|
|
212
|
-
player.setSelectedElementId(null);
|
|
213
|
-
player.setTimelineReady(false);
|
|
210
|
+
usePlayerStore.getState().setSelectedElementId(null);
|
|
214
211
|
return;
|
|
215
212
|
}
|
|
216
213
|
// A soft restore patched the reverted attributes onto the live preview, but the
|
|
217
214
|
// player store keeps its own copy and that copy is what the automation lanes
|
|
218
215
|
// draw — so without this an undone envelope edit stayed invisible until a
|
|
219
|
-
// reload. The full path above
|
|
216
|
+
// reload. The full path above waits for the reloaded preview to report instead.
|
|
220
217
|
syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null);
|
|
221
218
|
},
|
|
222
219
|
[previewIframeRef, activeCompPathRef, reloadPreview],
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import type { TimelineElement } from "../player";
|
|
10
10
|
import type { TimelineClipRenderContext } from "../player/components/TimelineTypes";
|
|
11
11
|
import { audioPillFlags } from "../player/components/audioClipLink";
|
|
12
|
-
import { AudioWaveform } from "../player/components/AudioWaveform";
|
|
12
|
+
import { AudioWaveform, rendersWaveform } from "../player/components/AudioWaveform";
|
|
13
13
|
import { ImageThumbnail } from "../player/components/ImageThumbnail";
|
|
14
14
|
import { encodePreviewPath, resolveMediaPreviewUrl } from "../player/components/thumbnailUtils";
|
|
15
15
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
@@ -135,7 +135,7 @@ export function useRenderClipContent({
|
|
|
135
135
|
// Thumbnail generation disabled (perf) -> plain clip bars. Audio still shows
|
|
136
136
|
// its waveform (cheap, not a frame thumbnail). Toggle: timeline toolbar.
|
|
137
137
|
if (effectiveMode === "hidden") {
|
|
138
|
-
return el
|
|
138
|
+
return rendersWaveform(el)
|
|
139
139
|
? renderAudioClip(el, pid, sessionEpoch, style.label, context, elements)
|
|
140
140
|
: null;
|
|
141
141
|
}
|
|
@@ -173,7 +173,7 @@ export function useRenderClipContent({
|
|
|
173
173
|
// Audio clips — waveform visualization. Resolve these before the generic
|
|
174
174
|
// activePreviewUrl thumbnail branch; audio rows need waveform data, not a
|
|
175
175
|
// captured frame from the currently drilled composition preview.
|
|
176
|
-
if (el
|
|
176
|
+
if (rendersWaveform(el)) {
|
|
177
177
|
return renderAudioClip(el, pid, sessionEpoch, style.label, context, elements);
|
|
178
178
|
}
|
|
179
179
|
|
package/src/index.ts
CHANGED
|
@@ -181,6 +181,8 @@ export type { UseClipboardOptions } from "./hooks/useClipboard";
|
|
|
181
181
|
// DOM editing for a host outside EditorShell; the Commit hooks save without useDomEditSession.
|
|
182
182
|
export { useDomEditSession } from "./hooks/useDomEditSession";
|
|
183
183
|
export type { UseDomEditSessionParams } from "./hooks/useDomEditSession";
|
|
184
|
+
export { useLivePreviewIframe } from "./hooks/useLivePreviewIframe";
|
|
185
|
+
export type { PreviewPromotion } from "./player/sceneSwap";
|
|
184
186
|
export { usePreviewPersistence } from "./hooks/usePreviewPersistence";
|
|
185
187
|
export type { UsePreviewPersistenceParams } from "./hooks/usePreviewPersistence";
|
|
186
188
|
export { DomEditProvider, useDomEditSelectionContext } from "./contexts/DomEditContext";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
// Reads the preview on screen the way a host app does: by package name.
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { expect, expectTypeOf, it } from "vitest";
|
|
6
|
+
import { useLivePreviewIframe, type PreviewPromotion } from "@hyperframes/studio";
|
|
7
|
+
import {
|
|
8
|
+
makePreview,
|
|
9
|
+
mountPlayerWithPreview,
|
|
10
|
+
paintShadow,
|
|
11
|
+
} from "./player/hooks/timelinePlayerTestHarness";
|
|
12
|
+
|
|
13
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
14
|
+
|
|
15
|
+
it("gives a host the preview on screen after a real shadow reload is promoted", async () => {
|
|
16
|
+
expectTypeOf<PreviewPromotion>().toHaveProperty("live");
|
|
17
|
+
const host = makePreview("<h1>Title</h1>");
|
|
18
|
+
const player = mountPlayerWithPreview(host);
|
|
19
|
+
let live: HTMLIFrameElement | null = null;
|
|
20
|
+
function Probe() {
|
|
21
|
+
live = useLivePreviewIframe(host);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const root = createRoot(document.body.appendChild(document.createElement("div")));
|
|
25
|
+
act(() => root.render(<Probe />));
|
|
26
|
+
act(() => player.getApi().refreshPlayer());
|
|
27
|
+
const shadow = makePreview("<h1>Title</h1>", "?_t=1");
|
|
28
|
+
await paintShadow(player.getApi, shadow);
|
|
29
|
+
expect(player.getApi().iframeRef.current).toBe(shadow);
|
|
30
|
+
expect(live).toBe(shadow);
|
|
31
|
+
act(() => root.unmount());
|
|
32
|
+
act(() => player.root.unmount());
|
|
33
|
+
});
|
package/src/main.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { StrictMode } from "react";
|
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import { IconContext } from "@phosphor-icons/react";
|
|
4
4
|
import { StudioApp } from "./App";
|
|
5
|
+
import { ShowThemeToggle } from "./components/ThemeToggle";
|
|
5
6
|
import { StudioErrorBoundary } from "./components/StudioErrorBoundary";
|
|
6
7
|
import { readIconTokens } from "./styles/iconTokens";
|
|
7
8
|
import { trackStudioEvent } from "./utils/studioTelemetry";
|
|
@@ -128,7 +129,9 @@ createRoot(document.getElementById("root")!).render(
|
|
|
128
129
|
Icons that pass their own size or weight still win. */}
|
|
129
130
|
<IconContext.Provider value={readIconTokens()}>
|
|
130
131
|
<StudioErrorBoundary>
|
|
131
|
-
<
|
|
132
|
+
<ShowThemeToggle.Provider value>
|
|
133
|
+
<StudioApp />
|
|
134
|
+
</ShowThemeToggle.Provider>
|
|
132
135
|
</StudioErrorBoundary>
|
|
133
136
|
</IconContext.Provider>
|
|
134
137
|
</StrictMode>,
|