@hyperframes/studio 0.7.70 → 0.7.72
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-DpKeYLEo.js → hyperframes-player-Bjf2HPzR.js} +1 -1
- package/dist/assets/index-BgZMle9I.css +1 -0
- package/dist/assets/{index-DXjAllII.js → index-Ch1hbJ3e.js} +1 -1
- package/dist/assets/{index-CbX-xoud.js → index-Ct_pxETK.js} +1 -1
- package/dist/assets/index-DCyWLHnx.js +428 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.html +2 -2
- package/dist/index.js +5383 -3804
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -3
- package/src/components/StudioRightPanel.tsx +10 -4
- package/src/components/editor/AnimationCard.test.tsx +100 -0
- package/src/components/editor/AnimationCard.tsx +5 -2
- package/src/components/editor/EaseCurveSection.test.tsx +348 -0
- package/src/components/editor/EaseCurveSection.tsx +444 -237
- package/src/components/editor/EaseParamFields.test.tsx +188 -0
- package/src/components/editor/EaseParamFields.tsx +246 -0
- package/src/components/editor/PropertyPanel.test.tsx +9 -7
- package/src/components/editor/PropertyPanelFlat.tsx +74 -88
- package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
- package/src/components/editor/easeCurveSvg.tsx +65 -0
- package/src/components/editor/easePresetLibrary.test.ts +114 -0
- package/src/components/editor/easePresetLibrary.ts +49 -0
- package/src/components/editor/gsapAnimationConstants.ts +15 -13
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +73 -40
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +85 -66
- package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
- package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +5 -5
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
- package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
- package/src/components/editor/propertyPanelTypes.ts +13 -0
- package/src/components/editor/useColorGradingController.test.ts +235 -30
- package/src/components/editor/useColorGradingController.ts +46 -88
- package/src/components/editor/useColorGradingPreviews.ts +307 -0
- package/src/components/nle/NLEContext.tsx +1 -1
- package/src/contexts/PanelLayoutContext.tsx +3 -6
- package/src/hooks/useBlockCatalog.ts +31 -13
- package/src/hooks/useBlockHandlers.ts +22 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
- package/src/hooks/useGsapScriptCommits.ts +14 -1
- package/src/hooks/usePanelLayout.test.ts +68 -1
- package/src/hooks/usePanelLayout.ts +72 -34
- package/src/player/components/PlayerControls.tsx +68 -229
- package/src/player/components/ShortcutsPanel.tsx +2 -4
- package/src/player/components/SpeedMenu.tsx +1 -2
- package/src/telemetry/events.test.ts +9 -0
- package/src/telemetry/events.ts +5 -0
- package/src/utils/blockInstaller.test.ts +67 -0
- package/src/utils/blockInstaller.ts +162 -109
- package/src/utils/studioUiPreferences.test.ts +5 -0
- package/src/utils/studioUiPreferences.ts +8 -0
- package/dist/assets/index-CAeU317U.js +0 -428
- package/dist/assets/index-Ceyz8Qt2.css +0 -1
- package/src/player/components/PlayerControls.test.ts +0 -20
- package/src/player/components/useSeekBarDrag.ts +0 -169
|
@@ -9,6 +9,7 @@ import { addBlockToProject } from "../utils/blockInstaller";
|
|
|
9
9
|
import type { BlockParam } from "@hyperframes/core/registry";
|
|
10
10
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
11
11
|
import type { RightPanelTab } from "../utils/studioHelpers";
|
|
12
|
+
import type { MediaOverlayPlacement } from "../components/editor/propertyPanelTypes";
|
|
12
13
|
|
|
13
14
|
interface BlockCtxDeps {
|
|
14
15
|
activeCompPath: string | null;
|
|
@@ -46,6 +47,7 @@ export interface UseBlockHandlersResult {
|
|
|
46
47
|
>;
|
|
47
48
|
handleAddBlock: (blockName: string) => void;
|
|
48
49
|
handleTimelineBlockDrop: (blockName: string, placement: { start: number; track: number }) => void;
|
|
50
|
+
handleAddMediaOverlay: (blockName: string, placement: MediaOverlayPlacement) => Promise<void>;
|
|
49
51
|
handlePreviewBlockDrop: (blockName: string, position: { left: number; top: number }) => void;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -151,6 +153,25 @@ export function useBlockHandlers({
|
|
|
151
153
|
[projectId, blockCtx, previewIframeRef, runBlockInstall],
|
|
152
154
|
);
|
|
153
155
|
|
|
156
|
+
const handleAddMediaOverlay = useCallback(
|
|
157
|
+
async (blockName: string, placement: MediaOverlayPlacement) => {
|
|
158
|
+
if (!projectId) return;
|
|
159
|
+
const { compositionPath, ...timelinePlacement } = placement;
|
|
160
|
+
await runBlockInstall(blockName, () =>
|
|
161
|
+
addBlockToProject({
|
|
162
|
+
projectId,
|
|
163
|
+
blockName,
|
|
164
|
+
...blockCtx,
|
|
165
|
+
activeCompPath: compositionPath ?? blockCtx.activeCompPath,
|
|
166
|
+
placement: timelinePlacement,
|
|
167
|
+
previewIframe: previewIframeRef.current,
|
|
168
|
+
currentTime: usePlayerStore.getState().currentTime,
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
[projectId, blockCtx, previewIframeRef, runBlockInstall],
|
|
173
|
+
);
|
|
174
|
+
|
|
154
175
|
const handlePreviewBlockDrop = useCallback(
|
|
155
176
|
(blockName: string, position: { left: number; top: number }) => {
|
|
156
177
|
if (!projectId) return;
|
|
@@ -173,6 +194,7 @@ export function useBlockHandlers({
|
|
|
173
194
|
setActiveBlockParams,
|
|
174
195
|
handleAddBlock,
|
|
175
196
|
handleTimelineBlockDrop,
|
|
197
|
+
handleAddMediaOverlay,
|
|
176
198
|
handlePreviewBlockDrop,
|
|
177
199
|
};
|
|
178
200
|
}
|
|
@@ -283,6 +283,21 @@ function mockFetchResult(over: Partial<MutationResult> = {}): void {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
describe("runCommit — instantPatch wiring", () => {
|
|
286
|
+
it("explains a deliberate mutation that the server safely rejected as unchanged", async () => {
|
|
287
|
+
mockFetchResult({ changed: false });
|
|
288
|
+
const deps = renderCommitHook();
|
|
289
|
+
|
|
290
|
+
await act(async () => {
|
|
291
|
+
await deps.api.commitMutation(
|
|
292
|
+
selection,
|
|
293
|
+
{ type: "move-keyframe", fromPercentage: 50, toPercentage: 100 },
|
|
294
|
+
{ label: "Move keyframe" },
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(deps.showToast).toHaveBeenCalledWith("A keyframe already exists at that time", "info");
|
|
299
|
+
});
|
|
300
|
+
|
|
286
301
|
it("no-op commit with an instantPatch still patches the runtime (paired x/y commits)", async () => {
|
|
287
302
|
patchRuntimeTweenInPlace.mockReturnValue(true);
|
|
288
303
|
mockFetchResult({ changed: false });
|
|
@@ -77,6 +77,17 @@ async function mutateGsapScriptBatch(
|
|
|
77
77
|
|
|
78
78
|
type ShowToast = (message: string, tone?: "error" | "info") => void;
|
|
79
79
|
|
|
80
|
+
function showUnchangedMutationFeedback(
|
|
81
|
+
mutations: Record<string, unknown>[],
|
|
82
|
+
result: MutationResult,
|
|
83
|
+
showToast: ShowToast | undefined,
|
|
84
|
+
): void {
|
|
85
|
+
if (result.changed !== false || mutations.length !== 1) return;
|
|
86
|
+
if (mutations[0]?.type === "move-keyframe") {
|
|
87
|
+
showToast?.("A keyframe already exists at that time", "info");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
async function runMutationRequest(
|
|
81
92
|
mutations: Record<string, unknown>[],
|
|
82
93
|
options: CommitMutationOptions,
|
|
@@ -94,7 +105,9 @@ async function runMutationRequest(
|
|
|
94
105
|
);
|
|
95
106
|
}
|
|
96
107
|
try {
|
|
97
|
-
|
|
108
|
+
const result = await request();
|
|
109
|
+
showUnchangedMutationFeedback(mutations, result, showToast);
|
|
110
|
+
return result;
|
|
98
111
|
} catch (error) {
|
|
99
112
|
if (error instanceof GsapMutationHttpError) {
|
|
100
113
|
showToast?.(formatGsapMutationRejectionToast(error), "error");
|
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { readStudioUiPreferences } from "../utils/studioUiPreferences";
|
|
6
7
|
import { usePanelLayout } from "./usePanelLayout";
|
|
7
8
|
|
|
8
9
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
10
|
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
const entries = new Map<string, string>();
|
|
13
|
+
Object.defineProperty(window, "localStorage", {
|
|
14
|
+
configurable: true,
|
|
15
|
+
value: {
|
|
16
|
+
get length() {
|
|
17
|
+
return entries.size;
|
|
18
|
+
},
|
|
19
|
+
clear: () => entries.clear(),
|
|
20
|
+
getItem: (key: string) => entries.get(key) ?? null,
|
|
21
|
+
key: (index: number) => Array.from(entries.keys())[index] ?? null,
|
|
22
|
+
removeItem: (key: string) => entries.delete(key),
|
|
23
|
+
setItem: (key: string, value: string) => entries.set(key, value),
|
|
24
|
+
} satisfies Storage,
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1496 });
|
|
27
|
+
});
|
|
28
|
+
|
|
10
29
|
afterEach(() => {
|
|
11
30
|
document.body.innerHTML = "";
|
|
12
31
|
vi.doUnmock("../components/editor/manualEditingAvailability");
|
|
@@ -42,6 +61,54 @@ function renderPanelLayout() {
|
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
describe("usePanelLayout — right inspector panes", () => {
|
|
64
|
+
it("opens Design with the intended viewport-scaled panel widths", () => {
|
|
65
|
+
const harness = renderPanelLayout();
|
|
66
|
+
|
|
67
|
+
expect(harness.getState()).toMatchObject({
|
|
68
|
+
leftWidth: 384,
|
|
69
|
+
rightWidth: 424,
|
|
70
|
+
rightCollapsed: false,
|
|
71
|
+
rightPanelTab: "design",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
harness.unmount();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("persists the latest pointer width even before React rerenders", () => {
|
|
78
|
+
const harness = renderPanelLayout();
|
|
79
|
+
const state = harness.getState();
|
|
80
|
+
const target = { setPointerCapture: vi.fn() };
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
state.handlePanelResizeStart("left", {
|
|
84
|
+
preventDefault: vi.fn(),
|
|
85
|
+
target,
|
|
86
|
+
pointerId: 1,
|
|
87
|
+
clientX: 100,
|
|
88
|
+
} as unknown as React.PointerEvent);
|
|
89
|
+
state.handlePanelResizeMove({ clientX: 140 } as React.PointerEvent);
|
|
90
|
+
state.handlePanelResizeEnd();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(harness.getState().leftWidth).toBe(424);
|
|
94
|
+
expect(readStudioUiPreferences().leftWidth).toBe(424);
|
|
95
|
+
harness.unmount();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("accumulates and persists rapid keyboard resize steps", () => {
|
|
99
|
+
const harness = renderPanelLayout();
|
|
100
|
+
const state = harness.getState();
|
|
101
|
+
|
|
102
|
+
act(() => {
|
|
103
|
+
state.adjustPanelWidth("right", 16);
|
|
104
|
+
state.adjustPanelWidth("right", 16);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(harness.getState().rightWidth).toBe(456);
|
|
108
|
+
expect(readStudioUiPreferences().rightWidth).toBe(456);
|
|
109
|
+
harness.unmount();
|
|
110
|
+
});
|
|
111
|
+
|
|
45
112
|
it("toggleRightInspectorPane independently flips one pane, allowing both open at once", () => {
|
|
46
113
|
const harness = renderPanelLayout();
|
|
47
114
|
expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true });
|
|
@@ -13,30 +13,76 @@ export interface InitialPanelLayoutState {
|
|
|
13
13
|
rightPanelTab?: RightPanelTab | null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
type PanelSide = "left" | "right";
|
|
17
|
+
|
|
16
18
|
function getInitialRightInspectorPanes(tab?: RightPanelTab | null): RightInspectorPanes {
|
|
17
19
|
if (tab === "layers") return { layers: true, design: false };
|
|
18
20
|
return { layers: false, design: true };
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function getInitialPanelWidths(): { left: number; right: number } {
|
|
24
|
+
const viewportWidth = typeof window === "undefined" ? 1496 : window.innerWidth;
|
|
25
|
+
const preferences = readStudioUiPreferences();
|
|
26
|
+
const leftDefault = Math.max(240, Math.min(384, Math.round(viewportWidth * 0.257)));
|
|
27
|
+
const rightDefault = Math.max(320, Math.min(424, Math.round(viewportWidth * 0.284)));
|
|
28
|
+
return {
|
|
29
|
+
left: Math.max(
|
|
30
|
+
160,
|
|
31
|
+
Math.min(Math.floor(viewportWidth * 0.5), preferences.leftWidth ?? leftDefault),
|
|
32
|
+
),
|
|
33
|
+
right: Math.max(160, Math.min(600, preferences.rightWidth ?? rightDefault)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function clampPanelWidth(side: PanelSide, width: number): number {
|
|
38
|
+
const max = side === "left" ? Math.floor(window.innerWidth * 0.5) : 600;
|
|
39
|
+
return Math.max(160, Math.min(max, width));
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
export function usePanelLayout(initialState?: InitialPanelLayoutState) {
|
|
22
|
-
const [
|
|
23
|
-
const [
|
|
43
|
+
const [initialPanelWidths] = useState(getInitialPanelWidths);
|
|
44
|
+
const [leftWidth, setLeftWidth] = useState(initialPanelWidths.left);
|
|
45
|
+
const [rightWidth, setRightWidth] = useState(initialPanelWidths.right);
|
|
46
|
+
const panelWidthsRef = useRef(initialPanelWidths);
|
|
24
47
|
const [leftCollapsed, setLeftCollapsed] = useState(
|
|
25
48
|
() => readStudioUiPreferences().leftCollapsed ?? false,
|
|
26
49
|
);
|
|
27
|
-
const [rightCollapsed, setRightCollapsed] = useState(initialState?.rightCollapsed ??
|
|
50
|
+
const [rightCollapsed, setRightCollapsed] = useState(initialState?.rightCollapsed ?? false);
|
|
28
51
|
const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab>(
|
|
29
|
-
initialState?.rightPanelTab ?? "
|
|
52
|
+
initialState?.rightPanelTab ?? "design",
|
|
30
53
|
);
|
|
31
54
|
const [rightInspectorPanes, setRightInspectorPanes] = useState<RightInspectorPanes>(() =>
|
|
32
55
|
getInitialRightInspectorPanes(initialState?.rightPanelTab),
|
|
33
56
|
);
|
|
34
57
|
const panelDragRef = useRef<{
|
|
35
|
-
side:
|
|
58
|
+
side: PanelSide;
|
|
36
59
|
startX: number;
|
|
37
60
|
startW: number;
|
|
38
61
|
} | null>(null);
|
|
39
62
|
|
|
63
|
+
const updatePanelWidth = useCallback((side: PanelSide, width: number) => {
|
|
64
|
+
const next = clampPanelWidth(side, width);
|
|
65
|
+
panelWidthsRef.current[side] = next;
|
|
66
|
+
if (side === "left") setLeftWidth(next);
|
|
67
|
+
else setRightWidth(next);
|
|
68
|
+
return next;
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const commitPanelWidth = useCallback(
|
|
72
|
+
(side: PanelSide, width: number) => {
|
|
73
|
+
const next = updatePanelWidth(side, Math.round(width));
|
|
74
|
+
writeStudioUiPreferences(side === "left" ? { leftWidth: next } : { rightWidth: next });
|
|
75
|
+
},
|
|
76
|
+
[updatePanelWidth],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const adjustPanelWidth = useCallback(
|
|
80
|
+
(side: PanelSide, delta: number) => {
|
|
81
|
+
commitPanelWidth(side, panelWidthsRef.current[side] + delta);
|
|
82
|
+
},
|
|
83
|
+
[commitPanelWidth],
|
|
84
|
+
);
|
|
85
|
+
|
|
40
86
|
const toggleLeftSidebar = useCallback(() => {
|
|
41
87
|
setLeftCollapsed((collapsed) => {
|
|
42
88
|
writeStudioUiPreferences({ leftCollapsed: !collapsed });
|
|
@@ -45,38 +91,31 @@ export function usePanelLayout(initialState?: InitialPanelLayoutState) {
|
|
|
45
91
|
});
|
|
46
92
|
}, []);
|
|
47
93
|
|
|
48
|
-
const handlePanelResizeStart = useCallback(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
const handlePanelResizeStart = useCallback((side: PanelSide, e: React.PointerEvent) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
97
|
+
panelDragRef.current = {
|
|
98
|
+
side,
|
|
99
|
+
startX: e.clientX,
|
|
100
|
+
startW: panelWidthsRef.current[side],
|
|
101
|
+
};
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const handlePanelResizeMove = useCallback(
|
|
105
|
+
(e: React.PointerEvent) => {
|
|
106
|
+
const drag = panelDragRef.current;
|
|
107
|
+
if (!drag) return;
|
|
108
|
+
const delta = e.clientX - drag.startX;
|
|
109
|
+
updatePanelWidth(drag.side, drag.startW + (drag.side === "left" ? delta : -delta));
|
|
57
110
|
},
|
|
58
|
-
[
|
|
111
|
+
[updatePanelWidth],
|
|
59
112
|
);
|
|
60
113
|
|
|
61
|
-
const handlePanelResizeMove = useCallback((e: React.PointerEvent) => {
|
|
62
|
-
const drag = panelDragRef.current;
|
|
63
|
-
if (!drag) return;
|
|
64
|
-
const delta = e.clientX - drag.startX;
|
|
65
|
-
const maxLeft = Math.floor(window.innerWidth * 0.5);
|
|
66
|
-
const newW = Math.max(
|
|
67
|
-
160,
|
|
68
|
-
Math.min(
|
|
69
|
-
drag.side === "left" ? maxLeft : 600,
|
|
70
|
-
drag.startW + (drag.side === "left" ? delta : -delta),
|
|
71
|
-
),
|
|
72
|
-
);
|
|
73
|
-
if (drag.side === "left") setLeftWidth(newW);
|
|
74
|
-
else setRightWidth(newW);
|
|
75
|
-
}, []);
|
|
76
|
-
|
|
77
114
|
const handlePanelResizeEnd = useCallback(() => {
|
|
115
|
+
const side = panelDragRef.current?.side;
|
|
116
|
+
if (side) commitPanelWidth(side, panelWidthsRef.current[side]);
|
|
78
117
|
panelDragRef.current = null;
|
|
79
|
-
}, []);
|
|
118
|
+
}, [commitPanelWidth]);
|
|
80
119
|
|
|
81
120
|
const trackedSetRightPanelTab = useCallback(
|
|
82
121
|
(tab: RightPanelTab) => {
|
|
@@ -120,9 +159,8 @@ export function usePanelLayout(initialState?: InitialPanelLayoutState) {
|
|
|
120
159
|
|
|
121
160
|
return {
|
|
122
161
|
leftWidth,
|
|
123
|
-
setLeftWidth,
|
|
124
162
|
rightWidth,
|
|
125
|
-
|
|
163
|
+
adjustPanelWidth,
|
|
126
164
|
leftCollapsed,
|
|
127
165
|
setLeftCollapsed,
|
|
128
166
|
rightCollapsed,
|