@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.
Files changed (62) hide show
  1. package/dist/assets/{hyperframes-player-DpKeYLEo.js → hyperframes-player-Bjf2HPzR.js} +1 -1
  2. package/dist/assets/index-BgZMle9I.css +1 -0
  3. package/dist/assets/{index-DXjAllII.js → index-Ch1hbJ3e.js} +1 -1
  4. package/dist/assets/{index-CbX-xoud.js → index-Ct_pxETK.js} +1 -1
  5. package/dist/assets/index-DCyWLHnx.js +428 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +5383 -3804
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +4 -4
  12. package/src/components/StudioLeftSidebar.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +10 -4
  14. package/src/components/editor/AnimationCard.test.tsx +100 -0
  15. package/src/components/editor/AnimationCard.tsx +5 -2
  16. package/src/components/editor/EaseCurveSection.test.tsx +348 -0
  17. package/src/components/editor/EaseCurveSection.tsx +444 -237
  18. package/src/components/editor/EaseParamFields.test.tsx +188 -0
  19. package/src/components/editor/EaseParamFields.tsx +246 -0
  20. package/src/components/editor/PropertyPanel.test.tsx +9 -7
  21. package/src/components/editor/PropertyPanelFlat.tsx +74 -88
  22. package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
  23. package/src/components/editor/easeCurveSvg.tsx +65 -0
  24. package/src/components/editor/easePresetLibrary.test.ts +114 -0
  25. package/src/components/editor/easePresetLibrary.ts +49 -0
  26. package/src/components/editor/gsapAnimationConstants.ts +15 -13
  27. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +73 -40
  28. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +85 -66
  29. package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
  30. package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
  31. package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
  32. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
  33. package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
  34. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
  35. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +5 -5
  36. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
  37. package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
  38. package/src/components/editor/propertyPanelTypes.ts +13 -0
  39. package/src/components/editor/useColorGradingController.test.ts +235 -30
  40. package/src/components/editor/useColorGradingController.ts +46 -88
  41. package/src/components/editor/useColorGradingPreviews.ts +307 -0
  42. package/src/components/nle/NLEContext.tsx +1 -1
  43. package/src/contexts/PanelLayoutContext.tsx +3 -6
  44. package/src/hooks/useBlockCatalog.ts +31 -13
  45. package/src/hooks/useBlockHandlers.ts +22 -0
  46. package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
  47. package/src/hooks/useGsapScriptCommits.ts +14 -1
  48. package/src/hooks/usePanelLayout.test.ts +68 -1
  49. package/src/hooks/usePanelLayout.ts +72 -34
  50. package/src/player/components/PlayerControls.tsx +68 -229
  51. package/src/player/components/ShortcutsPanel.tsx +2 -4
  52. package/src/player/components/SpeedMenu.tsx +1 -2
  53. package/src/telemetry/events.test.ts +9 -0
  54. package/src/telemetry/events.ts +5 -0
  55. package/src/utils/blockInstaller.test.ts +67 -0
  56. package/src/utils/blockInstaller.ts +162 -109
  57. package/src/utils/studioUiPreferences.test.ts +5 -0
  58. package/src/utils/studioUiPreferences.ts +8 -0
  59. package/dist/assets/index-CAeU317U.js +0 -428
  60. package/dist/assets/index-Ceyz8Qt2.css +0 -1
  61. package/src/player/components/PlayerControls.test.ts +0 -20
  62. 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
- return await request();
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 [leftWidth, setLeftWidth] = useState(240);
23
- const [rightWidth, setRightWidth] = useState(400);
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 ?? true);
50
+ const [rightCollapsed, setRightCollapsed] = useState(initialState?.rightCollapsed ?? false);
28
51
  const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab>(
29
- initialState?.rightPanelTab ?? "renders",
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: "left" | "right";
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
- (side: "left" | "right", e: React.PointerEvent) => {
50
- e.preventDefault();
51
- (e.target as HTMLElement).setPointerCapture(e.pointerId);
52
- panelDragRef.current = {
53
- side,
54
- startX: e.clientX,
55
- startW: side === "left" ? leftWidth : rightWidth,
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
- [leftWidth, rightWidth],
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
- setRightWidth,
163
+ adjustPanelWidth,
126
164
  leftCollapsed,
127
165
  setLeftCollapsed,
128
166
  rightCollapsed,