@hyperframes/studio 0.7.58 → 0.7.60
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-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.ts +2 -2
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +4 -1
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
const trackDesignInput = vi.fn();
|
|
8
|
+
vi.mock("../utils/designInputTracking", () => ({
|
|
9
|
+
trackDesignInput: (...args: unknown[]) => trackDesignInput(...args),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { DesignPanelInputProvider, useTrackDesignInput } from "./DesignPanelInputContext";
|
|
13
|
+
|
|
14
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => trackDesignInput.mockReset());
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
document.body.innerHTML = "";
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function FireButton({ control, name }: { control: string; name: string }) {
|
|
22
|
+
const track = useTrackDesignInput();
|
|
23
|
+
return (
|
|
24
|
+
<button type="button" onClick={() => track(control, name)}>
|
|
25
|
+
fire
|
|
26
|
+
</button>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderAndClick(tree: React.ReactElement) {
|
|
31
|
+
const host = document.createElement("div");
|
|
32
|
+
document.body.appendChild(host);
|
|
33
|
+
const root = createRoot(host);
|
|
34
|
+
act(() => root.render(tree));
|
|
35
|
+
act(() => {
|
|
36
|
+
host.querySelector("button")!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
37
|
+
});
|
|
38
|
+
act(() => root.unmount());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("DesignPanelInputContext", () => {
|
|
42
|
+
it("binds the tracker to the enclosing ui + section", () => {
|
|
43
|
+
renderAndClick(
|
|
44
|
+
<DesignPanelInputProvider ui="flat" section="style">
|
|
45
|
+
<FireButton control="metric" name="Opacity" />
|
|
46
|
+
</DesignPanelInputProvider>,
|
|
47
|
+
);
|
|
48
|
+
expect(trackDesignInput).toHaveBeenCalledWith({
|
|
49
|
+
ui: "flat",
|
|
50
|
+
section: "style",
|
|
51
|
+
control: "metric",
|
|
52
|
+
name: "Opacity",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("nested provider overrides section but inherits ui from parent", () => {
|
|
57
|
+
renderAndClick(
|
|
58
|
+
<DesignPanelInputProvider ui="flat" section="outer">
|
|
59
|
+
<DesignPanelInputProvider section="color-grading">
|
|
60
|
+
<FireButton control="slider" name="Exposure" />
|
|
61
|
+
</DesignPanelInputProvider>
|
|
62
|
+
</DesignPanelInputProvider>,
|
|
63
|
+
);
|
|
64
|
+
expect(trackDesignInput).toHaveBeenCalledWith({
|
|
65
|
+
ui: "flat",
|
|
66
|
+
section: "color-grading",
|
|
67
|
+
control: "slider",
|
|
68
|
+
name: "Exposure",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("defaults to classic/unknown with no provider", () => {
|
|
73
|
+
renderAndClick(<FireButton control="button" name="Reset" />);
|
|
74
|
+
expect(trackDesignInput).toHaveBeenCalledWith({
|
|
75
|
+
ui: "classic",
|
|
76
|
+
section: "unknown",
|
|
77
|
+
control: "button",
|
|
78
|
+
name: "Reset",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useMemo, type ReactNode } from "react";
|
|
2
|
+
import { trackDesignInput, type DesignInputUi } from "../utils/designInputTracking";
|
|
3
|
+
|
|
4
|
+
// Carries which inspector UI and which section the currently-rendered design-panel
|
|
5
|
+
// inputs belong to, so commit sites only pass { control, name } and never thread
|
|
6
|
+
// section/ui through every call. Providers nest: PropertyPanel sets `ui` once at the
|
|
7
|
+
// top; each Section sets `section` and inherits `ui` from the parent.
|
|
8
|
+
|
|
9
|
+
interface DesignPanelInputContextValue {
|
|
10
|
+
ui: DesignInputUi;
|
|
11
|
+
section: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DesignPanelInputContext = createContext<DesignPanelInputContextValue>({
|
|
15
|
+
ui: "classic",
|
|
16
|
+
section: "unknown",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function DesignPanelInputProvider({
|
|
20
|
+
ui,
|
|
21
|
+
section,
|
|
22
|
+
children,
|
|
23
|
+
}: {
|
|
24
|
+
ui?: DesignInputUi;
|
|
25
|
+
section?: string;
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}) {
|
|
28
|
+
const parent = useContext(DesignPanelInputContext);
|
|
29
|
+
const value = useMemo(
|
|
30
|
+
() => ({ ui: ui ?? parent.ui, section: section ?? parent.section }),
|
|
31
|
+
[ui, section, parent.ui, parent.section],
|
|
32
|
+
);
|
|
33
|
+
return (
|
|
34
|
+
<DesignPanelInputContext.Provider value={value}>{children}</DesignPanelInputContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns a stable `track(control, name)` fn bound to the current UI + section.
|
|
40
|
+
* Call it from any input's commit handler.
|
|
41
|
+
*/
|
|
42
|
+
export function useTrackDesignInput(): (control: string, name: string) => void {
|
|
43
|
+
const { ui, section } = useContext(DesignPanelInputContext);
|
|
44
|
+
return useCallback(
|
|
45
|
+
(control: string, name: string) => trackDesignInput({ ui, section, control, name }),
|
|
46
|
+
[ui, section],
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface DomEditActionsValue extends Pick<
|
|
|
16
16
|
| "handleDomAttributeCommit"
|
|
17
17
|
| "handleDomAttributeLiveCommit"
|
|
18
18
|
| "handleDomHtmlAttributeCommit"
|
|
19
|
+
| "handleDomAttributesCommit"
|
|
19
20
|
| "handleDomPathOffsetCommit"
|
|
20
21
|
| "handleDomGroupPathOffsetCommit"
|
|
21
22
|
| "handleDomZIndexReorderCommit"
|
|
@@ -138,6 +139,7 @@ export function DomEditProvider({
|
|
|
138
139
|
handleDomAttributeCommit,
|
|
139
140
|
handleDomAttributeLiveCommit,
|
|
140
141
|
handleDomHtmlAttributeCommit,
|
|
142
|
+
handleDomAttributesCommit,
|
|
141
143
|
handleDomPathOffsetCommit,
|
|
142
144
|
handleDomGroupPathOffsetCommit,
|
|
143
145
|
handleDomZIndexReorderCommit,
|
|
@@ -224,6 +226,7 @@ export function DomEditProvider({
|
|
|
224
226
|
handleDomAttributeCommit,
|
|
225
227
|
handleDomAttributeLiveCommit,
|
|
226
228
|
handleDomHtmlAttributeCommit,
|
|
229
|
+
handleDomAttributesCommit,
|
|
227
230
|
handleDomPathOffsetCommit,
|
|
228
231
|
handleDomGroupPathOffsetCommit,
|
|
229
232
|
handleDomZIndexReorderCommit,
|
|
@@ -291,6 +294,7 @@ export function DomEditProvider({
|
|
|
291
294
|
handleDomAttributeCommit,
|
|
292
295
|
handleDomAttributeLiveCommit,
|
|
293
296
|
handleDomHtmlAttributeCommit,
|
|
297
|
+
handleDomAttributesCommit,
|
|
294
298
|
handleDomPathOffsetCommit,
|
|
295
299
|
handleDomGroupPathOffsetCommit,
|
|
296
300
|
handleDomZIndexReorderCommit,
|
|
@@ -30,6 +30,7 @@ export function FileManagerProvider({
|
|
|
30
30
|
readProjectFile,
|
|
31
31
|
writeProjectFile,
|
|
32
32
|
readOptionalProjectFile,
|
|
33
|
+
observeProjectFileVersion,
|
|
33
34
|
updateEditingFileContent,
|
|
34
35
|
revealSourceOffset,
|
|
35
36
|
openSourceForSelection,
|
|
@@ -69,6 +70,7 @@ export function FileManagerProvider({
|
|
|
69
70
|
readProjectFile,
|
|
70
71
|
writeProjectFile,
|
|
71
72
|
readOptionalProjectFile,
|
|
73
|
+
observeProjectFileVersion,
|
|
72
74
|
updateEditingFileContent,
|
|
73
75
|
revealSourceOffset,
|
|
74
76
|
openSourceForSelection,
|
|
@@ -102,6 +104,7 @@ export function FileManagerProvider({
|
|
|
102
104
|
readProjectFile,
|
|
103
105
|
writeProjectFile,
|
|
104
106
|
readOptionalProjectFile,
|
|
107
|
+
observeProjectFileVersion,
|
|
105
108
|
updateEditingFileContent,
|
|
106
109
|
revealSourceOffset,
|
|
107
110
|
openSourceForSelection,
|
|
@@ -25,6 +25,7 @@ export function PanelLayoutProvider({
|
|
|
25
25
|
setRightPanelTab,
|
|
26
26
|
rightInspectorPanes,
|
|
27
27
|
toggleRightInspectorPane,
|
|
28
|
+
setExclusiveRightInspectorPane,
|
|
28
29
|
toggleLeftSidebar,
|
|
29
30
|
handlePanelResizeStart,
|
|
30
31
|
handlePanelResizeMove,
|
|
@@ -49,6 +50,7 @@ export function PanelLayoutProvider({
|
|
|
49
50
|
setRightPanelTab,
|
|
50
51
|
rightInspectorPanes,
|
|
51
52
|
toggleRightInspectorPane,
|
|
53
|
+
setExclusiveRightInspectorPane,
|
|
52
54
|
toggleLeftSidebar,
|
|
53
55
|
handlePanelResizeStart,
|
|
54
56
|
handlePanelResizeMove,
|
|
@@ -67,6 +69,7 @@ export function PanelLayoutProvider({
|
|
|
67
69
|
setRightPanelTab,
|
|
68
70
|
rightInspectorPanes,
|
|
69
71
|
toggleRightInspectorPane,
|
|
72
|
+
setExclusiveRightInspectorPane,
|
|
70
73
|
toggleLeftSidebar,
|
|
71
74
|
handlePanelResizeStart,
|
|
72
75
|
handlePanelResizeMove,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { Composition } from "@hyperframes/sdk";
|
|
6
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
|
+
import {
|
|
8
|
+
VariablePromoteProvider,
|
|
9
|
+
useVariablePromoteChannel,
|
|
10
|
+
type ChannelPromote,
|
|
11
|
+
} from "./VariablePromoteContext";
|
|
12
|
+
|
|
13
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
|
+
|
|
15
|
+
describe("VariablePromoteProvider save failures", () => {
|
|
16
|
+
it("reports a rejected promote instead of leaving an unhandled promise", async () => {
|
|
17
|
+
const snapshot = {
|
|
18
|
+
attributes: { id: "box" },
|
|
19
|
+
inlineStyles: {},
|
|
20
|
+
text: "",
|
|
21
|
+
children: [],
|
|
22
|
+
};
|
|
23
|
+
const session = {
|
|
24
|
+
getElement: vi.fn().mockReturnValue(snapshot),
|
|
25
|
+
getVariableDeclarations: vi.fn().mockReturnValue([]),
|
|
26
|
+
on: vi.fn().mockReturnValue(() => {}),
|
|
27
|
+
} as unknown as Composition;
|
|
28
|
+
const selection = {
|
|
29
|
+
hfId: "hf-box",
|
|
30
|
+
tagName: "div",
|
|
31
|
+
label: "Box",
|
|
32
|
+
capabilities: { canEditStyles: true },
|
|
33
|
+
computedStyles: { color: "rgb(255, 0, 0)" },
|
|
34
|
+
} as unknown as DomEditSelection;
|
|
35
|
+
const error = new Error("write failed");
|
|
36
|
+
const persist = vi.fn().mockRejectedValue(error);
|
|
37
|
+
const onPersistError = vi.fn();
|
|
38
|
+
let channel: ChannelPromote | null = null;
|
|
39
|
+
|
|
40
|
+
function Consumer() {
|
|
41
|
+
channel = useVariablePromoteChannel({ kind: "style", prop: "color" });
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const root = createRoot(document.createElement("div"));
|
|
46
|
+
act(() => {
|
|
47
|
+
root.render(
|
|
48
|
+
<VariablePromoteProvider
|
|
49
|
+
session={session}
|
|
50
|
+
selection={selection}
|
|
51
|
+
persist={persist}
|
|
52
|
+
onPersistError={onPersistError}
|
|
53
|
+
>
|
|
54
|
+
<Consumer />
|
|
55
|
+
</VariablePromoteProvider>,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
act(() => channel?.promote());
|
|
59
|
+
await act(async () => {
|
|
60
|
+
await Promise.resolve();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(persist).toHaveBeenCalledOnce();
|
|
64
|
+
expect(onPersistError).toHaveBeenCalledWith(error);
|
|
65
|
+
act(() => root.unmount());
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -44,6 +44,7 @@ interface VariablePromoteContextValue {
|
|
|
44
44
|
actions: BindAction[];
|
|
45
45
|
declarations: CompositionVariable[];
|
|
46
46
|
persist: (label: string, mutate: (session: Composition) => void) => Promise<boolean>;
|
|
47
|
+
onPersistError: (error: unknown) => void;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const VariablePromoteContext = createContext<VariablePromoteContextValue | null>(null);
|
|
@@ -58,11 +59,13 @@ export function VariablePromoteProvider({
|
|
|
58
59
|
session,
|
|
59
60
|
selection,
|
|
60
61
|
persist,
|
|
62
|
+
onPersistError,
|
|
61
63
|
children,
|
|
62
64
|
}: {
|
|
63
65
|
session: Composition | null;
|
|
64
66
|
selection: DomEditSelection | null;
|
|
65
67
|
persist: (label: string, mutate: (session: Composition) => void) => Promise<boolean>;
|
|
68
|
+
onPersistError: (error: unknown) => void;
|
|
66
69
|
children: React.ReactNode;
|
|
67
70
|
}) {
|
|
68
71
|
// Re-derive actions/bindings after each persisted schema edit.
|
|
@@ -85,8 +88,8 @@ export function VariablePromoteProvider({
|
|
|
85
88
|
}, [session, revision]);
|
|
86
89
|
|
|
87
90
|
const value = useMemo<VariablePromoteContextValue>(
|
|
88
|
-
() => ({ session, selection, actions, declarations, persist }),
|
|
89
|
-
[session, selection, actions, declarations, persist],
|
|
91
|
+
() => ({ session, selection, actions, declarations, persist, onPersistError }),
|
|
92
|
+
[session, selection, actions, declarations, persist, onPersistError],
|
|
90
93
|
);
|
|
91
94
|
|
|
92
95
|
return (
|
|
@@ -105,7 +108,7 @@ export function useVariablePromoteChannel(channel: PromoteChannel): ChannelPromo
|
|
|
105
108
|
|
|
106
109
|
return useMemo(() => {
|
|
107
110
|
if (!ctx || !ctx.session || !ctx.selection?.hfId) return null;
|
|
108
|
-
const { session, selection, actions, declarations, persist } = ctx;
|
|
111
|
+
const { session, selection, actions, declarations, persist, onPersistError } = ctx;
|
|
109
112
|
const hfId = selection.hfId!;
|
|
110
113
|
const action = matchAction(actions, channel);
|
|
111
114
|
const boundId = readBinding(session, hfId, channel);
|
|
@@ -125,12 +128,14 @@ export function useVariablePromoteChannel(channel: PromoteChannel): ChannelPromo
|
|
|
125
128
|
const id = uniqueId(action.suggestedId, declarations);
|
|
126
129
|
void persist(`Bind ${action.label.toLowerCase()} to variable "${id}"`, (s) =>
|
|
127
130
|
applyBind(s, hfId, action, id),
|
|
128
|
-
);
|
|
131
|
+
).catch(onPersistError);
|
|
129
132
|
},
|
|
130
133
|
setDefault: (raw: string) => {
|
|
131
134
|
if (!boundId || !declaration) return;
|
|
132
135
|
const next = declaration.type === "color" ? rgbToHex(raw) : raw;
|
|
133
|
-
void persist(`Set default for "${boundId}"`, (s) =>
|
|
136
|
+
void persist(`Set default for "${boundId}"`, (s) =>
|
|
137
|
+
s.setVariableValue(boundId, next),
|
|
138
|
+
).catch(onPersistError);
|
|
134
139
|
},
|
|
135
140
|
};
|
|
136
141
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -7,6 +7,15 @@ interface DomEditCommitRunnerConfig {
|
|
|
7
7
|
onError: (error: unknown) => void;
|
|
8
8
|
shouldResync: () => boolean;
|
|
9
9
|
resync: () => void | Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Reports success/failure without changing this function's own resolve-
|
|
12
|
+
* always contract — `persist` failures are handled here (revert + onError)
|
|
13
|
+
* and never rethrown, so callers awaiting `runDomEditCommit` can't observe
|
|
14
|
+
* failure via rejection. A caller that needs to react to a specific
|
|
15
|
+
* commit's outcome (e.g. reverting its OWN optimistic state) can pass this
|
|
16
|
+
* instead of relying on a rejection that will never come.
|
|
17
|
+
*/
|
|
18
|
+
onSettled?: (ok: boolean) => void;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
interface CommitVersionRef {
|
|
@@ -34,11 +43,13 @@ export async function runDomEditCommit(config: DomEditCommitRunnerConfig): Promi
|
|
|
34
43
|
|
|
35
44
|
try {
|
|
36
45
|
await config.persist();
|
|
46
|
+
config.onSettled?.(true);
|
|
37
47
|
} catch (error) {
|
|
38
48
|
if (config.shouldRevert(error)) {
|
|
39
49
|
config.revert();
|
|
40
50
|
}
|
|
41
51
|
config.onError(error);
|
|
52
|
+
config.onSettled?.(false);
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
if (!config.shouldResync()) return;
|
|
@@ -2,6 +2,7 @@ import type { ParsedGsap } from "@hyperframes/core/gsap-parser";
|
|
|
2
2
|
import type { Composition } from "@hyperframes/sdk";
|
|
3
3
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
4
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
5
|
+
import type { PublishSdkSession } from "../utils/sdkCutover";
|
|
5
6
|
import type { RuntimeTweenChange } from "./gsapRuntimePatch";
|
|
6
7
|
|
|
7
8
|
export interface MutationResult {
|
|
@@ -22,10 +23,9 @@ export interface CommitMutationOptions {
|
|
|
22
23
|
beforeReload?: () => void;
|
|
23
24
|
/**
|
|
24
25
|
* Serialize this commit against others sharing the same key. Used to chain
|
|
25
|
-
* per-animationId GSAP meta updates
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* (and under distinct keys) run concurrently as before.
|
|
26
|
+
* per-animationId GSAP meta updates. Every commit independently takes the
|
|
27
|
+
* project/file mutation lock, so this key only adds ordering and can never
|
|
28
|
+
* bypass whole-file serialization.
|
|
29
29
|
*/
|
|
30
30
|
serializeKey?: string;
|
|
31
31
|
/**
|
|
@@ -87,6 +87,8 @@ export interface GsapScriptCommitsParams {
|
|
|
87
87
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
88
88
|
/** Stage 7 §3.5: SDK session for routing GSAP tween ops through addGsapTween/setGsapTween/removeGsapTween. */
|
|
89
89
|
sdkSession?: Composition | null;
|
|
90
|
+
/** Publish a fully persisted candidate SDK session. */
|
|
91
|
+
publishSdkSession?: PublishSdkSession;
|
|
90
92
|
writeProjectFile?: (path: string, content: string) => Promise<void>;
|
|
91
93
|
/** Resync the in-memory SDK session after a server-authoritative write. */
|
|
92
94
|
forceReloadSdkSession?: () => void;
|
|
@@ -262,7 +262,7 @@ export interface PersistTimelineEditInput {
|
|
|
262
262
|
activeCompPath: string | null;
|
|
263
263
|
label: string;
|
|
264
264
|
buildPatches: (original: string, target: PatchTarget) => string;
|
|
265
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
265
|
+
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
266
266
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
267
267
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
268
268
|
pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
|
|
@@ -308,7 +308,7 @@ export interface PersistTimelineBatchEditInput {
|
|
|
308
308
|
activeCompPath: string | null;
|
|
309
309
|
label: string;
|
|
310
310
|
changes: PersistTimelineBatchChange[];
|
|
311
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
311
|
+
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
312
312
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
313
313
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
314
314
|
pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
|
|
@@ -201,4 +201,56 @@ describe("toggleTimelineElementHidden", () => {
|
|
|
201
201
|
usePlayerStore.getState().elements.find((el) => el.key === "index.html:#track-mate")?.hidden,
|
|
202
202
|
).toBeUndefined();
|
|
203
203
|
});
|
|
204
|
+
|
|
205
|
+
it("hides several elements in ONE atomic write when given an array of keys", async () => {
|
|
206
|
+
const files = new Map([
|
|
207
|
+
[
|
|
208
|
+
"index.html",
|
|
209
|
+
`<div id="hero" data-start="0" data-duration="2"></div>
|
|
210
|
+
<div id="caption" data-start="1" data-duration="2"></div>
|
|
211
|
+
<div id="badge" data-start="2" data-duration="2"></div>`,
|
|
212
|
+
],
|
|
213
|
+
]);
|
|
214
|
+
stubProjectFiles(files);
|
|
215
|
+
|
|
216
|
+
const hero = element({ id: "hero", key: "index.html:#hero", domId: "hero", track: 0 });
|
|
217
|
+
const caption = element({
|
|
218
|
+
id: "caption",
|
|
219
|
+
key: "index.html:#caption",
|
|
220
|
+
domId: "caption",
|
|
221
|
+
track: 1,
|
|
222
|
+
});
|
|
223
|
+
const badge = element({ id: "badge", key: "index.html:#badge", domId: "badge", track: 2 });
|
|
224
|
+
|
|
225
|
+
const writes: Array<{ path: string; content: string }> = [];
|
|
226
|
+
const recordEdit = vi.fn();
|
|
227
|
+
|
|
228
|
+
await toggleTimelineElementHidden({
|
|
229
|
+
projectId: "project-1",
|
|
230
|
+
activeCompPath: "index.html",
|
|
231
|
+
timelineElements: [hero, caption, badge],
|
|
232
|
+
elementKey: ["index.html:#hero", "index.html:#caption"],
|
|
233
|
+
hidden: true,
|
|
234
|
+
previewIframe: null,
|
|
235
|
+
writeProjectFile: async (path, content) => {
|
|
236
|
+
writes.push({ path, content });
|
|
237
|
+
},
|
|
238
|
+
recordEdit,
|
|
239
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
240
|
+
pendingTimelineEditPathRef: { current: new Set() },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// One write carrying BOTH hides — per-element writes would clobber each
|
|
244
|
+
// other (each starts from the original file content).
|
|
245
|
+
expect(writes).toHaveLength(1);
|
|
246
|
+
expect(writes[0]?.content).toContain(
|
|
247
|
+
'id="hero" data-start="0" data-duration="2" data-hidden=""',
|
|
248
|
+
);
|
|
249
|
+
expect(writes[0]?.content).toContain(
|
|
250
|
+
'id="caption" data-start="1" data-duration="2" data-hidden=""',
|
|
251
|
+
);
|
|
252
|
+
expect(writes[0]?.content).toContain('id="badge" data-start="2" data-duration="2"></div>');
|
|
253
|
+
expect(recordEdit).toHaveBeenCalledTimes(1);
|
|
254
|
+
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide 2 elements");
|
|
255
|
+
});
|
|
204
256
|
});
|
|
@@ -33,7 +33,8 @@ interface ToggleTimelineTrackHiddenInput {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
interface ToggleTimelineElementHiddenInput extends Omit<ToggleTimelineTrackHiddenInput, "track"> {
|
|
36
|
-
|
|
36
|
+
/** One timeline key, or several to hide/show in a single atomic file write. */
|
|
37
|
+
elementKey: string | readonly string[];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
interface SetElementsHiddenInput {
|
|
@@ -62,7 +63,7 @@ interface UseTimelineTrackVisibilityEditingInput extends Omit<
|
|
|
62
63
|
|
|
63
64
|
interface UseTimelineElementVisibilityEditingInput extends Omit<
|
|
64
65
|
ToggleTimelineElementHiddenInput,
|
|
65
|
-
"projectId" | "elementKey" | "hidden" | "previewIframe"
|
|
66
|
+
"projectId" | "elementKey" | "hidden" | "previewIframe" | "timelineElements"
|
|
66
67
|
> {
|
|
67
68
|
projectIdRef: ReadonlyRef<string | null>;
|
|
68
69
|
previewIframeRef: ReadonlyRef<HTMLIFrameElement | null>;
|
|
@@ -233,13 +234,21 @@ export async function toggleTimelineElementHidden({
|
|
|
233
234
|
domEditSaveTimestampRef,
|
|
234
235
|
pendingTimelineEditPathRef,
|
|
235
236
|
}: ToggleTimelineElementHiddenInput): Promise<string[]> {
|
|
236
|
-
const
|
|
237
|
+
const keys = new Set(typeof elementKey === "string" ? [elementKey] : elementKey);
|
|
238
|
+
const elements = timelineElements.filter((item) => keys.has(item.key ?? item.id));
|
|
237
239
|
return setElementsHidden({
|
|
238
240
|
projectId,
|
|
239
241
|
activeCompPath,
|
|
240
|
-
elements
|
|
242
|
+
elements,
|
|
241
243
|
hidden,
|
|
242
|
-
label:
|
|
244
|
+
label:
|
|
245
|
+
elements.length > 1
|
|
246
|
+
? hidden
|
|
247
|
+
? `Hide ${elements.length} elements`
|
|
248
|
+
: `Show ${elements.length} elements`
|
|
249
|
+
: hidden
|
|
250
|
+
? "Hide element"
|
|
251
|
+
: "Show element",
|
|
243
252
|
previewIframe,
|
|
244
253
|
writeProjectFile,
|
|
245
254
|
recordEdit,
|
|
@@ -313,7 +322,6 @@ export function useTimelineTrackVisibilityEditing({
|
|
|
313
322
|
export function useTimelineElementVisibilityEditing({
|
|
314
323
|
projectIdRef,
|
|
315
324
|
activeCompPath,
|
|
316
|
-
timelineElements,
|
|
317
325
|
showToast,
|
|
318
326
|
writeProjectFile,
|
|
319
327
|
recordEdit,
|
|
@@ -323,11 +331,20 @@ export function useTimelineElementVisibilityEditing({
|
|
|
323
331
|
isRecordingRef,
|
|
324
332
|
forceReloadSdkSession,
|
|
325
333
|
}: UseTimelineElementVisibilityEditingInput): (
|
|
326
|
-
elementKey: string,
|
|
334
|
+
elementKey: string | readonly string[],
|
|
327
335
|
hidden: boolean,
|
|
328
336
|
) => Promise<void> {
|
|
337
|
+
// Resolve against the EXPANDED rows, not the raw store list — a nested
|
|
338
|
+
// sub-composition child has no entry of its own in the raw list (only its
|
|
339
|
+
// host does), so an elementKey for such a child (the
|
|
340
|
+
// `sourceFile#domId`-shaped virtual key `resolveTimelineIdForSelection`
|
|
341
|
+
// falls back to) would never match anything there and Hide All would
|
|
342
|
+
// silently no-op for it. The expanded list synthesizes a real, patchable
|
|
343
|
+
// TimelineElement (with matching key/domId/sourceFile) for each visible
|
|
344
|
+
// child whenever its host is currently expanded.
|
|
345
|
+
const expandedElements = useExpandedTimelineElements();
|
|
329
346
|
return useCallback(
|
|
330
|
-
async (elementKey: string, hidden: boolean) => {
|
|
347
|
+
async (elementKey: string | readonly string[], hidden: boolean) => {
|
|
331
348
|
if (isRecordingRef?.current) {
|
|
332
349
|
showToast("Cannot edit timeline while recording", "error");
|
|
333
350
|
return;
|
|
@@ -338,7 +355,7 @@ export function useTimelineElementVisibilityEditing({
|
|
|
338
355
|
await toggleTimelineElementHidden({
|
|
339
356
|
projectId: pid,
|
|
340
357
|
activeCompPath,
|
|
341
|
-
timelineElements,
|
|
358
|
+
timelineElements: expandedElements,
|
|
342
359
|
elementKey,
|
|
343
360
|
hidden,
|
|
344
361
|
previewIframe: previewIframeRef.current,
|
|
@@ -357,7 +374,7 @@ export function useTimelineElementVisibilityEditing({
|
|
|
357
374
|
},
|
|
358
375
|
[
|
|
359
376
|
activeCompPath,
|
|
360
|
-
|
|
377
|
+
expandedElements,
|
|
361
378
|
previewIframeRef,
|
|
362
379
|
writeProjectFile,
|
|
363
380
|
recordEdit,
|
|
@@ -9,6 +9,7 @@ import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
|
|
|
9
9
|
import { canSplitElement } from "../utils/timelineElementSplit";
|
|
10
10
|
import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
11
11
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
12
|
+
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
12
13
|
|
|
13
14
|
function iframeContentWindow(iframe: HTMLIFrameElement | null): Window | null {
|
|
14
15
|
try {
|
|
@@ -87,6 +88,7 @@ interface HistoryResult {
|
|
|
87
88
|
interface HistoryFileCallbacks {
|
|
88
89
|
readFile: (path: string) => Promise<string>;
|
|
89
90
|
writeFile: (path: string, content: string) => Promise<void>;
|
|
91
|
+
serialize?: <T>(paths: readonly string[], task: () => Promise<T>) => Promise<T>;
|
|
90
92
|
}
|
|
91
93
|
interface EditHistoryHandle {
|
|
92
94
|
undo: (cb: HistoryFileCallbacks) => Promise<HistoryResult>;
|
|
@@ -367,6 +369,11 @@ export function useAppHotkeys({
|
|
|
367
369
|
},
|
|
368
370
|
[domEditSaveTimestampRef, writeProjectFile],
|
|
369
371
|
);
|
|
372
|
+
const serializeHistoryFiles = useCallback(
|
|
373
|
+
<T>(paths: readonly string[], task: () => Promise<T>) =>
|
|
374
|
+
serializeStudioFileMutations(writeProjectFile, paths, task),
|
|
375
|
+
[writeProjectFile],
|
|
376
|
+
);
|
|
370
377
|
|
|
371
378
|
const applyHistory = useCallback(
|
|
372
379
|
async (direction: "undo" | "redo") => {
|
|
@@ -377,6 +384,7 @@ export function useAppHotkeys({
|
|
|
377
384
|
const result = await editHistory[direction]({
|
|
378
385
|
readFile: readHistoryFile,
|
|
379
386
|
writeFile: writeHistoryFile,
|
|
387
|
+
serialize: serializeHistoryFiles,
|
|
380
388
|
});
|
|
381
389
|
if (!result.ok && result.reason === "content-mismatch") {
|
|
382
390
|
showToast(
|
|
@@ -406,6 +414,7 @@ export function useAppHotkeys({
|
|
|
406
414
|
syncHistoryPreviewAfterApply,
|
|
407
415
|
waitForPendingDomEditSaves,
|
|
408
416
|
writeHistoryFile,
|
|
417
|
+
serializeHistoryFiles,
|
|
409
418
|
onAfterUndoRedo,
|
|
410
419
|
activeCompPath,
|
|
411
420
|
forceReloadSdkSession,
|