@hyperframes/studio 0.7.71 → 0.7.73

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 (79) hide show
  1. package/dist/assets/{hyperframes-player-C1X90psg.js → hyperframes-player-DGlzhf_s.js} +1 -1
  2. package/dist/assets/index-B37rWXo4.css +1 -0
  3. package/dist/assets/{index-DzxDHPR1.js → index-CbVTOJ-E.js} +1 -1
  4. package/dist/assets/{index-nY4lLBqM.js → index-CcA5WVJv.js} +1 -1
  5. package/dist/assets/index-DXcLI2wu.js +428 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7762 -4261
  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 +75 -88
  22. package/src/components/editor/colorGradingFrameAnalysis.test.ts +101 -0
  23. package/src/components/editor/colorGradingFrameAnalysis.ts +203 -0
  24. package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
  25. package/src/components/editor/easeCurveSvg.tsx +65 -0
  26. package/src/components/editor/easePresetLibrary.test.ts +114 -0
  27. package/src/components/editor/easePresetLibrary.ts +49 -0
  28. package/src/components/editor/gsapAnimationConstants.ts +15 -13
  29. package/src/components/editor/propertyPanelColorCurveGraph.tsx +549 -0
  30. package/src/components/editor/propertyPanelColorCurves.test.tsx +158 -0
  31. package/src/components/editor/propertyPanelColorCurves.tsx +185 -0
  32. package/src/components/editor/propertyPanelColorGradingControls.tsx +115 -77
  33. package/src/components/editor/propertyPanelColorScopes.tsx +258 -0
  34. package/src/components/editor/propertyPanelColorSecondary.test.tsx +181 -0
  35. package/src/components/editor/propertyPanelColorSecondary.tsx +448 -0
  36. package/src/components/editor/propertyPanelColorWheels.test.tsx +119 -0
  37. package/src/components/editor/propertyPanelColorWheels.tsx +334 -0
  38. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +109 -0
  39. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +239 -41
  40. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +260 -300
  41. package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
  42. package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
  43. package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
  44. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
  45. package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
  46. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
  47. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
  48. package/src/components/editor/propertyPanelGradingNumberField.test.tsx +79 -0
  49. package/src/components/editor/propertyPanelGradingNumberField.tsx +116 -0
  50. package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
  51. package/src/components/editor/propertyPanelTypes.ts +13 -0
  52. package/src/components/editor/useColorGradingController.test.ts +331 -10
  53. package/src/components/editor/useColorGradingController.ts +57 -90
  54. package/src/components/editor/useColorGradingPreviews.ts +350 -0
  55. package/src/components/editor/useColorGradingScopes.test.tsx +143 -0
  56. package/src/components/editor/useColorGradingScopes.ts +62 -0
  57. package/src/components/editor/useInspectorGestureTransaction.ts +30 -1
  58. package/src/components/nle/NLEContext.tsx +1 -1
  59. package/src/contexts/PanelLayoutContext.tsx +3 -6
  60. package/src/hooks/useBlockCatalog.ts +31 -13
  61. package/src/hooks/useBlockHandlers.ts +22 -0
  62. package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
  63. package/src/hooks/useGsapScriptCommits.ts +14 -1
  64. package/src/hooks/usePanelLayout.test.ts +68 -1
  65. package/src/hooks/usePanelLayout.ts +72 -34
  66. package/src/icons/SystemIcons.tsx +4 -0
  67. package/src/player/components/PlayerControls.tsx +68 -229
  68. package/src/player/components/ShortcutsPanel.tsx +2 -4
  69. package/src/player/components/SpeedMenu.tsx +1 -2
  70. package/src/telemetry/events.test.ts +9 -0
  71. package/src/telemetry/events.ts +5 -0
  72. package/src/utils/blockInstaller.test.ts +67 -0
  73. package/src/utils/blockInstaller.ts +162 -109
  74. package/src/utils/studioUiPreferences.test.ts +5 -0
  75. package/src/utils/studioUiPreferences.ts +8 -0
  76. package/dist/assets/index-CSCh2Vrp.js +0 -428
  77. package/dist/assets/index-Ceyz8Qt2.css +0 -1
  78. package/src/player/components/PlayerControls.test.ts +0 -20
  79. package/src/player/components/useSeekBarDrag.ts +0 -169
@@ -13,9 +13,9 @@ function brightPopGrading() {
13
13
  return next;
14
14
  }
15
15
 
16
- function warmDaylightGrading() {
17
- const next = normalizeHfColorGrading({ preset: "warm-daylight", intensity: 1 });
18
- if (!next) throw new Error("expected warm-daylight preset to normalize");
16
+ function cleanStudioGrading() {
17
+ const next = normalizeHfColorGrading({ preset: "clean-studio", intensity: 1 });
18
+ if (!next) throw new Error("expected clean-studio preset to normalize");
19
19
  return next;
20
20
  }
21
21
 
@@ -56,19 +56,30 @@ function makeElement(overrides: Partial<DomEditSelection> = {}): DomEditSelectio
56
56
  } as DomEditSelection;
57
57
  }
58
58
 
59
+ type ApplyScope = (
60
+ scope: "source-file" | "project",
61
+ value: string | null,
62
+ ) => Promise<{ changedFiles: number; changedElements: number }>;
63
+
59
64
  function HookHost({
60
65
  onState,
61
66
  onSetAttributeLive,
62
67
  element,
68
+ previewIframeRef,
69
+ onApplyScope,
63
70
  }: {
64
71
  onState: (state: ReturnType<typeof useColorGradingController>) => void;
65
72
  onSetAttributeLive: (attr: string, value: string | null) => void;
66
73
  element: DomEditSelection;
74
+ previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
75
+ onApplyScope?: ApplyScope;
67
76
  }) {
68
77
  const state = useColorGradingController({
69
78
  projectId: "proj",
70
79
  element,
80
+ previewIframeRef,
71
81
  onSetAttributeLive,
82
+ onApplyScope,
72
83
  });
73
84
  onState(state);
74
85
  return null;
@@ -77,6 +88,8 @@ function HookHost({
77
88
  function renderHook(
78
89
  onSetAttributeLive: (attr: string, value: string | null) => void,
79
90
  initialElement: DomEditSelection = makeElement(),
91
+ previewIframeRef?: React.RefObject<HTMLIFrameElement | null>,
92
+ onApplyScope?: ApplyScope,
80
93
  ) {
81
94
  const host = document.createElement("div");
82
95
  document.body.append(host);
@@ -89,6 +102,8 @@ function renderHook(
89
102
  onState: (s: ReturnType<typeof useColorGradingController>) => (latest = s),
90
103
  onSetAttributeLive,
91
104
  element,
105
+ previewIframeRef,
106
+ onApplyScope,
92
107
  }),
93
108
  );
94
109
  });
@@ -107,6 +122,58 @@ function renderHook(
107
122
  };
108
123
  }
109
124
 
125
+ type PreviewWindow = Window & {
126
+ __hf?: {
127
+ colorGrading?: {
128
+ renderPreviews?: ReturnType<typeof vi.fn>;
129
+ startPreviewPlayback?: ReturnType<typeof vi.fn>;
130
+ };
131
+ };
132
+ __player?: { play: ReturnType<typeof vi.fn> };
133
+ };
134
+
135
+ function createPreviewFrame() {
136
+ const iframe = document.body.appendChild(document.createElement("iframe"));
137
+ const contentWindow = iframe.contentWindow as PreviewWindow | null;
138
+ if (!contentWindow) throw new Error("expected iframe contentWindow");
139
+ return { contentWindow, iframe };
140
+ }
141
+
142
+ function installPreviewRenderer(contentWindow: PreviewWindow) {
143
+ const renderPreviews = vi
144
+ .fn()
145
+ .mockImplementation(async (_target: unknown, candidates: Array<{ id: string }>) => ({
146
+ width: 160,
147
+ height: 90,
148
+ images: candidates.map(({ id }) => ({
149
+ id,
150
+ dataUrl: `data:image/png;base64,${id}`,
151
+ })),
152
+ }));
153
+ contentWindow.__hf = { colorGrading: { renderPreviews } };
154
+ return renderPreviews;
155
+ }
156
+
157
+ async function flushPreviewRequest() {
158
+ act(() => vi.advanceTimersByTime(0));
159
+ await act(async () => {
160
+ await Promise.resolve();
161
+ await Promise.resolve();
162
+ });
163
+ }
164
+
165
+ function colorGradingMessages(calls: ReadonlyArray<ReadonlyArray<unknown>>) {
166
+ return calls
167
+ .map(([message]) => message)
168
+ .filter(
169
+ (message): message is { action: string; grading: unknown } =>
170
+ typeof message === "object" &&
171
+ message !== null &&
172
+ "action" in message &&
173
+ message.action === "set-color-grading",
174
+ );
175
+ }
176
+
110
177
  describe("useColorGradingController", () => {
111
178
  it("starts with the neutral (inactive) grading and idle compare state", () => {
112
179
  const { root, getState } = renderHook(vi.fn());
@@ -115,6 +182,168 @@ describe("useColorGradingController", () => {
115
182
  act(() => root.unmount());
116
183
  });
117
184
 
185
+ it("requests one exact preset batch from the selected media runtime", async () => {
186
+ vi.useFakeTimers();
187
+ const devicePixelRatio = vi.spyOn(window, "devicePixelRatio", "get").mockReturnValue(2);
188
+ const { contentWindow, iframe } = createPreviewFrame();
189
+ const renderPreviews = installPreviewRenderer(contentWindow);
190
+ const initialElement = makeElement({
191
+ dataAttributes: {
192
+ "color-grading": JSON.stringify({
193
+ wheels: { shadows: { hue: 210, amount: 0.2 } },
194
+ effects: { pixelate: 0.4 },
195
+ palette: ["#112233", "#ffffff"],
196
+ }),
197
+ },
198
+ });
199
+ const { root, getState } = renderHook(vi.fn(), initialElement, { current: iframe });
200
+
201
+ act(() => getState().requestPresetPreviews());
202
+ await flushPreviewRequest();
203
+
204
+ expect(renderPreviews).toHaveBeenCalledTimes(1);
205
+ expect(renderPreviews.mock.calls[0]?.[1]).toHaveLength(18);
206
+ expect(renderPreviews.mock.calls[0]?.[1]).toContainEqual({
207
+ id: "bright-pop",
208
+ grading: expect.objectContaining({
209
+ wheels: expect.objectContaining({
210
+ shadows: expect.objectContaining({ amount: 0 }),
211
+ }),
212
+ effects: expect.objectContaining({ pixelate: 0.4 }),
213
+ palette: ["#112233", "#ffffff"],
214
+ }),
215
+ });
216
+ expect(renderPreviews.mock.calls[0]?.[2]).toEqual({ maxDimension: 320 });
217
+ expect(getState().presetPreviews).toEqual({
218
+ status: "ready",
219
+ images: expect.objectContaining({ "bright-pop": "data:image/png;base64,bright-pop" }),
220
+ width: 160,
221
+ height: 90,
222
+ });
223
+ act(() => root.unmount());
224
+ devicePixelRatio.mockRestore();
225
+ vi.useRealTimers();
226
+ });
227
+
228
+ it("retains partial preset previews and exposes retry after the batch times out", async () => {
229
+ vi.useFakeTimers();
230
+ const { contentWindow, iframe } = createPreviewFrame();
231
+ const renderPreviews = vi.fn().mockResolvedValue({
232
+ width: 160,
233
+ height: 90,
234
+ images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,bright" }],
235
+ });
236
+ contentWindow.__hf = { colorGrading: { renderPreviews } };
237
+ const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
238
+
239
+ act(() => getState().requestPresetPreviews());
240
+ await flushPreviewRequest();
241
+ expect(getState().presetPreviews).toMatchObject({
242
+ status: "loading",
243
+ images: { "bright-pop": "data:image/png;base64,bright" },
244
+ });
245
+
246
+ await act(async () => {
247
+ await vi.advanceTimersByTimeAsync(1700);
248
+ });
249
+ expect(getState().presetPreviews).toMatchObject({
250
+ status: "unavailable",
251
+ images: { "bright-pop": "data:image/png;base64,bright" },
252
+ });
253
+ expect(renderPreviews.mock.calls.length).toBeGreaterThan(1);
254
+ act(() => root.unmount());
255
+ vi.useRealTimers();
256
+ });
257
+
258
+ it("persists a disabled secondary even though it does not render pixels", () => {
259
+ vi.useFakeTimers();
260
+ const onSetAttributeLive = vi.fn();
261
+ const grading = normalizeHfColorGrading({
262
+ secondaries: [
263
+ {
264
+ enabled: false,
265
+ key: { hue: { center: 215, range: 25 } },
266
+ correction: { saturation: 0.15 },
267
+ },
268
+ ],
269
+ });
270
+ if (!grading) throw new Error("expected secondary grading");
271
+ const { root, getState } = renderHook(onSetAttributeLive);
272
+
273
+ act(() => getState().commitColorGrading(grading));
274
+ act(() => vi.advanceTimersByTime(400));
275
+
276
+ expect(onSetAttributeLive.mock.calls[0]?.[0]).toBe("color-grading");
277
+ expect(onSetAttributeLive.mock.calls[0]?.[1]).toContain('"enabled":false');
278
+ act(() => root.unmount());
279
+ vi.useRealTimers();
280
+ });
281
+
282
+ it("copies a disabled authored secondary to a broader scope", async () => {
283
+ const onApplyScope = vi.fn<ApplyScope>().mockResolvedValue({
284
+ changedFiles: 1,
285
+ changedElements: 1,
286
+ });
287
+ const grading = {
288
+ secondaries: [
289
+ {
290
+ enabled: false,
291
+ key: { hue: { center: 215, range: 25 } },
292
+ correction: { saturation: 0.15 },
293
+ },
294
+ ],
295
+ };
296
+ const { root, getState } = renderHook(
297
+ vi.fn(),
298
+ makeElement({ dataAttributes: { "color-grading": JSON.stringify(grading) } }),
299
+ undefined,
300
+ onApplyScope,
301
+ );
302
+
303
+ await act(async () => getState().applyToScope());
304
+
305
+ expect(onApplyScope).toHaveBeenCalledWith(
306
+ "source-file",
307
+ expect.stringContaining('"enabled":false'),
308
+ );
309
+ act(() => root.unmount());
310
+ });
311
+
312
+ it("requests exact effect families and retains earlier family images", async () => {
313
+ vi.useFakeTimers();
314
+ const { contentWindow, iframe } = createPreviewFrame();
315
+ const renderPreviews = installPreviewRenderer(contentWindow);
316
+ const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
317
+
318
+ act(() => getState().requestEffectPreviews(["blur", "pixelate", "bloom"]));
319
+ await flushPreviewRequest();
320
+
321
+ expect(renderPreviews).toHaveBeenCalledTimes(1);
322
+ expect(renderPreviews.mock.calls[0]?.[1].map(({ id }: { id: string }) => id)).toEqual([
323
+ "blur",
324
+ "pixelate",
325
+ "bloom",
326
+ ]);
327
+
328
+ act(() => getState().requestEffectPreviews(["kuwahara"]));
329
+ await flushPreviewRequest();
330
+
331
+ expect(renderPreviews).toHaveBeenCalledTimes(2);
332
+ expect(getState().effectPreviews).toEqual({
333
+ status: "ready",
334
+ images: {
335
+ blur: "data:image/png;base64,blur",
336
+ pixelate: "data:image/png;base64,pixelate",
337
+ bloom: "data:image/png;base64,bloom",
338
+ kuwahara: "data:image/png;base64,kuwahara",
339
+ },
340
+ width: 160,
341
+ height: 90,
342
+ });
343
+ act(() => root.unmount());
344
+ vi.useRealTimers();
345
+ });
346
+
118
347
  it("commitColorGrading updates grading state synchronously and schedules a debounced persist", async () => {
119
348
  vi.useFakeTimers();
120
349
  const onSetAttributeLive = vi.fn();
@@ -135,6 +364,86 @@ describe("useColorGradingController", () => {
135
364
  vi.useRealTimers();
136
365
  });
137
366
 
367
+ it("previews through the runtime only and restores the committed grade without persisting", () => {
368
+ vi.useFakeTimers();
369
+ const onSetAttributeLive = vi.fn();
370
+ const { contentWindow, iframe } = createPreviewFrame();
371
+ const postMessage = vi.spyOn(contentWindow, "postMessage");
372
+ const { root, getState } = renderHook(onSetAttributeLive, makeElement(), {
373
+ current: iframe,
374
+ });
375
+
376
+ act(() => getState().previewColorGrading(brightPopGrading()));
377
+ act(() => getState().previewColorGrading(null));
378
+ const gradingMessages = colorGradingMessages(postMessage.mock.calls);
379
+ expect(gradingMessages).toHaveLength(2);
380
+ expect(gradingMessages[0]?.grading).toMatchObject({ preset: "bright-pop" });
381
+ expect(gradingMessages[1]?.grading).toBeNull();
382
+ expect(getState().grading.preset).toBe("neutral");
383
+ act(() => vi.advanceTimersByTime(500));
384
+ expect(onSetAttributeLive).not.toHaveBeenCalled();
385
+
386
+ act(() => root.unmount());
387
+ vi.useRealTimers();
388
+ });
389
+
390
+ it("animates only the selected video card without starting the project player", async () => {
391
+ vi.useFakeTimers();
392
+ const { contentWindow, iframe } = createPreviewFrame();
393
+ const stopPlayback = vi.fn();
394
+ const renderPreviews = vi.fn().mockResolvedValue({
395
+ width: 320,
396
+ height: 180,
397
+ images: [{ id: "bright-pop", dataUrl: "data:image/png;base64,animated" }],
398
+ });
399
+ const startPreviewPlayback = vi.fn(() => stopPlayback);
400
+ contentWindow.__hf = { colorGrading: { renderPreviews, startPreviewPlayback } };
401
+ contentWindow.__player = { play: vi.fn() };
402
+ const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
403
+
404
+ act(() =>
405
+ getState().previewColorGrading(brightPopGrading(), {
406
+ animatedPreview: { kind: "presets", id: "bright-pop" },
407
+ }),
408
+ );
409
+ await act(async () => {
410
+ await Promise.resolve();
411
+ await Promise.resolve();
412
+ });
413
+ expect(startPreviewPlayback).toHaveBeenCalledTimes(1);
414
+ expect(renderPreviews).toHaveBeenCalledWith(
415
+ expect.anything(),
416
+ [{ id: "bright-pop", grading: expect.objectContaining({ preset: "bright-pop" }) }],
417
+ { maxDimension: 160, useMediaTime: true },
418
+ );
419
+ expect(contentWindow.__player.play).not.toHaveBeenCalled();
420
+ expect(getState().presetPreviews.images["bright-pop"]).toBe("data:image/png;base64,animated");
421
+
422
+ act(() => getState().previewColorGrading(null));
423
+ expect(stopPlayback).toHaveBeenCalledTimes(1);
424
+
425
+ act(() => root.unmount());
426
+ vi.useRealTimers();
427
+ });
428
+
429
+ it("restores a just-committed look even before React renders the new state", () => {
430
+ vi.useFakeTimers();
431
+ const { contentWindow, iframe } = createPreviewFrame();
432
+ const postMessage = vi.spyOn(contentWindow, "postMessage");
433
+ const { root, getState } = renderHook(vi.fn(), makeElement(), { current: iframe });
434
+ const brightPop = brightPopGrading();
435
+
436
+ act(() => {
437
+ getState().commitColorGrading(brightPop);
438
+ getState().previewColorGrading(null);
439
+ });
440
+ const gradingMessages = colorGradingMessages(postMessage.mock.calls);
441
+ expect(gradingMessages.at(-1)?.grading).toMatchObject({ preset: "bright-pop" });
442
+
443
+ act(() => root.unmount());
444
+ vi.useRealTimers();
445
+ });
446
+
138
447
  it("reverts to the last confirmed-good grading via the real onSettled(false) signal (matches runDomEditCommit, which never rejects)", async () => {
139
448
  // The actual Studio commit runner (runDomEditCommit) catches persist
140
449
  // failures internally and always resolves — it reports outcome only
@@ -253,7 +562,7 @@ describe("useColorGradingController", () => {
253
562
  });
254
563
  },
255
564
  )
256
- // Edit B (warm-daylight): settles immediately and successfully.
565
+ // Edit B (clean-studio): settles immediately and successfully.
257
566
  .mockImplementationOnce(
258
567
  (_attr: string, _value: string | null, onSettled?: (ok: boolean) => void) => {
259
568
  onSettled?.(true);
@@ -272,13 +581,13 @@ describe("useColorGradingController", () => {
272
581
 
273
582
  // B commits on the SAME element before A's persist has settled.
274
583
  act(() => {
275
- getState().commitColorGrading(warmDaylightGrading());
584
+ getState().commitColorGrading(cleanStudioGrading());
276
585
  });
277
586
  act(() => {
278
587
  vi.advanceTimersByTime(400);
279
588
  });
280
589
  expect(onSetAttributeLive).toHaveBeenCalledTimes(2); // B's persist has already settled (mock resolves sync)
281
- expect(getState().grading.preset).toBe("warm-daylight");
590
+ expect(getState().grading.preset).toBe("clean-studio");
282
591
 
283
592
  // NOW A's stale persist finally settles as a FAILURE — must not revert
284
593
  // `grading` (which now correctly shows B's newer edit) back to the
@@ -292,20 +601,32 @@ describe("useColorGradingController", () => {
292
601
  await Promise.resolve();
293
602
  await Promise.resolve();
294
603
  });
295
- expect(getState().grading.preset).toBe("warm-daylight");
604
+ expect(getState().grading.preset).toBe("clean-studio");
296
605
  act(() => root.unmount());
297
606
  vi.useRealTimers();
298
607
  });
299
608
 
300
- it("resetGrading returns to the neutral preset", () => {
609
+ it("resetGrading resets Grade fields without clearing LUT, Effects, or Palette", () => {
301
610
  const { root, getState } = renderHook(vi.fn());
611
+ const grading = normalizeHfColorGrading({
612
+ preset: "bright-pop",
613
+ lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
614
+ effects: { pixelate: 0.5 },
615
+ palette: ["#112233", "#ffffff"],
616
+ });
617
+ if (!grading) throw new Error("expected grading to normalize");
302
618
  act(() => {
303
- getState().commitColorGrading(brightPopGrading());
619
+ getState().commitColorGrading(grading);
304
620
  });
305
621
  act(() => {
306
622
  getState().resetGrading();
307
623
  });
308
- expect(getState().grading.preset).toBe("neutral");
624
+ expect(getState().grading).toMatchObject({
625
+ preset: "neutral",
626
+ lut: { src: "assets/luts/custom.cube", intensity: 0.6 },
627
+ effects: { pixelate: 0.5 },
628
+ palette: ["#112233", "#ffffff"],
629
+ });
309
630
  act(() => root.unmount());
310
631
  });
311
632