@hyperframes/studio 0.7.81 → 0.7.83

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 (41) hide show
  1. package/dist/assets/{hyperframes-player-B32VouCm.js → hyperframes-player-DSRuJMfh.js} +1 -1
  2. package/dist/assets/{index-DCTAKfpa.js → index-BrWVfpCQ.js} +1 -1
  3. package/dist/assets/{index-B5u2_pSh.js → index-DRtvHA1J.js} +150 -150
  4. package/dist/assets/{index-HeUw0EKI.js → index-YZR6OfQ5.js} +1 -1
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +3057 -3101
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -7
  9. package/src/App.tsx +2 -5
  10. package/src/components/StudioHeader.tsx +3 -18
  11. package/src/components/StudioRightPanel.tsx +14 -22
  12. package/src/components/TimelineToolbar.tsx +120 -132
  13. package/src/components/editor/EaseCurveSection.tsx +9 -90
  14. package/src/components/editor/EaseModeControls.tsx +110 -0
  15. package/src/components/editor/PropertyPanel.test.tsx +2 -3
  16. package/src/components/editor/PropertyPanel.tsx +7 -12
  17. package/src/components/editor/PropertyPanelFlat.tsx +0 -2
  18. package/src/components/editor/floatingPanel.test.ts +20 -1
  19. package/src/components/editor/floatingPanel.ts +15 -0
  20. package/src/components/editor/manualEditingAvailability.test.ts +13 -40
  21. package/src/components/editor/manualEditingAvailability.ts +0 -41
  22. package/src/components/editor/propertyPanel3dTransform.tsx +1 -2
  23. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +1 -2
  24. package/src/components/nle/PreviewOverlays.tsx +8 -20
  25. package/src/components/renders/RenderQueue.test.tsx +62 -0
  26. package/src/components/renders/RenderQueue.tsx +60 -38
  27. package/src/components/renders/useRenderQueue.ts +5 -11
  28. package/src/components/sidebar/LeftSidebar.tsx +15 -22
  29. package/src/components/ui/Tooltip.tsx +25 -7
  30. package/src/hooks/useAppHotkeys.ts +1 -2
  31. package/src/hooks/useDomEditPreviewSync.ts +1 -2
  32. package/src/hooks/useDomEditWiring.ts +2 -3
  33. package/src/hooks/useDomSelection.ts +0 -27
  34. package/src/hooks/usePreviewInteraction.ts +2 -3
  35. package/src/hooks/useStudioContextValue.ts +3 -7
  36. package/src/player/components/Timeline.tsx +1 -2
  37. package/src/player/components/TimelineLanes.tsx +33 -35
  38. package/src/player/components/useAutoExpandKeyframedClips.ts +0 -2
  39. package/src/player/components/useTimelineTrackLayout.ts +1 -5
  40. package/src/utils/studioUrlState.test.ts +5 -8
  41. package/src/utils/studioUrlState.ts +1 -10
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Ease editor mode primitives: the mode radio group and the preset grid.
3
+ *
4
+ * Split out of `EaseCurveSection.tsx` to keep it under the 600-line cap
5
+ * (CI's file size check). Both components are presentational and stateless —
6
+ * they take the current selection and emit a committed ease string — so they
7
+ * carry the mode vocabulary (`EASE_MODES`, labels, per-mode defaults) with
8
+ * them rather than importing it back from the parent.
9
+ */
10
+
11
+ import { EASE_PRESETS } from "./easePresetLibrary";
12
+ import { MiniCurveSvg } from "./easeCurveSvg";
13
+ import { EASE_CURVES } from "./gsapAnimationConstants";
14
+
15
+ const EASE_MODES = ["curve", "spring", "wiggle"] as const;
16
+ export type EaseMode = (typeof EASE_MODES)[number];
17
+
18
+ export type Pts = [number, number, number, number];
19
+ export const DEFAULT_CURVE: Pts = EASE_CURVES["power2.out"];
20
+
21
+ export const MODE_LABELS = { curve: "Curve", spring: "Spring", wiggle: "Wiggle" } satisfies Record<
22
+ EaseMode,
23
+ string
24
+ >;
25
+
26
+ const DEFAULT_EASE_BY_MODE = {
27
+ curve: `custom(M0,0 C${DEFAULT_CURVE[0]},${DEFAULT_CURVE[1]} ${DEFAULT_CURVE[2]},${DEFAULT_CURVE[3]} 1,1)`,
28
+ spring: "spring(0.42)",
29
+ wiggle: "wiggle(3,easeInOut,0.12)",
30
+ } satisfies Record<EaseMode, string>;
31
+
32
+ export const EasePresetGrid = function EasePresetGrid({
33
+ kind,
34
+ currentEase,
35
+ onSelect,
36
+ }: {
37
+ kind: EaseMode;
38
+ currentEase: string;
39
+ onSelect: (ease: string) => void;
40
+ }) {
41
+ return (
42
+ <div className="mb-2 grid max-h-56 grid-cols-4 gap-1 overflow-y-auto pr-0.5">
43
+ {EASE_PRESETS.filter((preset) => preset.kind === kind).map((preset) => {
44
+ const isActive = currentEase === preset.ease;
45
+ return (
46
+ <button
47
+ key={preset.id}
48
+ type="button"
49
+ data-ease-preset-id={preset.id}
50
+ role="menuitemradio"
51
+ aria-checked={isActive}
52
+ tabIndex={isActive ? 0 : -1}
53
+ onClick={() => onSelect(preset.ease)}
54
+ className={`flex flex-col items-center gap-0.5 rounded-md p-1 transition-colors ${
55
+ isActive ? "bg-panel-accent/10 ring-1 ring-panel-accent/30" : "hover:bg-neutral-800"
56
+ }`}
57
+ title={preset.label}
58
+ >
59
+ <MiniCurveSvg ease={preset.ease} active={isActive} />
60
+ <span
61
+ className={`text-center text-[8px] leading-none ${
62
+ isActive ? "text-panel-accent" : "text-neutral-500"
63
+ }`}
64
+ >
65
+ {preset.label}
66
+ </span>
67
+ </button>
68
+ );
69
+ })}
70
+ </div>
71
+ );
72
+ };
73
+
74
+ export function EaseModeToggle({
75
+ mode,
76
+ onCommit,
77
+ }: {
78
+ mode: EaseMode;
79
+ onCommit: (ease: string) => void;
80
+ }) {
81
+ return (
82
+ <div
83
+ className="mb-2 grid grid-cols-3 rounded-md bg-black/20 p-0.5"
84
+ role="radiogroup"
85
+ aria-label="Ease editor mode"
86
+ >
87
+ {EASE_MODES.map((candidateMode) => {
88
+ const active = candidateMode === mode;
89
+ return (
90
+ <button
91
+ key={candidateMode}
92
+ type="button"
93
+ data-ease-mode={candidateMode}
94
+ role="radio"
95
+ aria-checked={active}
96
+ onClick={() => {
97
+ if (active) return;
98
+ onCommit(DEFAULT_EASE_BY_MODE[candidateMode]);
99
+ }}
100
+ className={`rounded px-2 py-1 text-[10px] font-medium transition-colors ${
101
+ active ? "bg-neutral-700 text-neutral-100" : "text-neutral-500 hover:text-neutral-300"
102
+ }`}
103
+ >
104
+ {MODE_LABELS[candidateMode]}
105
+ </button>
106
+ );
107
+ })}
108
+ </div>
109
+ );
110
+ }
@@ -447,9 +447,8 @@ describe("PropertyPanel — Motion group (Plan 3b)", () => {
447
447
  it(
448
448
  "hides the effect list (showEffects off) when the GSAP edit handlers are absent",
449
449
  async () => {
450
- // STUDIO_GSAP_PANEL_ENABLED defaults on, but none of the five required
451
- // edit handlers are supplied here, so the effect-list half of the
452
- // double-gate stays closed — only the Timing row shows.
450
+ // None of the five required edit handlers are supplied here, so the
451
+ // effect list stays closed only the Timing row shows.
453
452
  const { host, root } = await renderPanel(true, animatedElement());
454
453
  openFlatGroup(host, "Motion");
455
454
  const openGroup = openGroupText(host);
@@ -26,11 +26,7 @@ import { TextSection, StyleSections } from "./propertyPanelSections";
26
26
  import { GsapAnimationSection } from "./GsapAnimationSection";
27
27
  import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
28
28
  import { KeyframeNavigation } from "./KeyframeNavigation";
29
- import {
30
- STUDIO_FLAT_INSPECTOR_ENABLED,
31
- STUDIO_GSAP_PANEL_ENABLED,
32
- STUDIO_KEYFRAMES_ENABLED,
33
- } from "./manualEditingAvailability";
29
+ import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./manualEditingAvailability";
34
30
  import { PropertyPanelFlat } from "./PropertyPanelFlat";
35
31
  import { createGsapLivePreview } from "./gsapLivePreview";
36
32
  import { usePlayerStore, liveTime } from "../../player";
@@ -396,7 +392,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
396
392
  onCommit={(next) => commitManualOffset("x", next)}
397
393
  />
398
394
  </div>
399
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
395
+ {gsapAnimId && (
400
396
  <KeyframeNavigation
401
397
  property="x"
402
398
  keyframes={navKeyframes}
@@ -423,7 +419,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
423
419
  onCommit={(next) => commitManualOffset("y", next)}
424
420
  />
425
421
  </div>
426
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
422
+ {gsapAnimId && (
427
423
  <KeyframeNavigation
428
424
  property="y"
429
425
  keyframes={navKeyframes}
@@ -450,7 +446,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
450
446
  onCommit={(next) => commitManualSize("width", next)}
451
447
  />
452
448
  </div>
453
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
449
+ {gsapAnimId && (
454
450
  <KeyframeNavigation
455
451
  property="width"
456
452
  keyframes={navKeyframes}
@@ -477,7 +473,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
477
473
  onCommit={(next) => commitManualSize("height", next)}
478
474
  />
479
475
  </div>
480
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
476
+ {gsapAnimId && (
481
477
  <KeyframeNavigation
482
478
  property="height"
483
479
  keyframes={navKeyframes}
@@ -503,7 +499,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
503
499
  onCommit={(next) => commitManualRotation(next.replace("°", ""))}
504
500
  />
505
501
  </div>
506
- {STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
502
+ {gsapAnimId && (
507
503
  <KeyframeNavigation
508
504
  property="rotation"
509
505
  keyframes={navKeyframes}
@@ -551,8 +547,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
551
547
  </Section>
552
548
  )}
553
549
 
554
- {STUDIO_GSAP_PANEL_ENABLED &&
555
- onUpdateGsapProperty &&
550
+ {onUpdateGsapProperty &&
556
551
  onUpdateGsapMeta &&
557
552
  onDeleteGsapAnimation &&
558
553
  onAddGsapProperty &&
@@ -16,7 +16,6 @@ import { FlatMediaSection } from "./propertyPanelFlatMediaSection";
16
16
  import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation";
17
17
  import { createGsapLivePreview } from "./gsapLivePreview";
18
18
  import { formatTextFieldPreview } from "./propertyPanelSections";
19
- import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
20
19
  import { useColorGradingController } from "./useColorGradingController";
21
20
  import { usePlayerStore } from "../../player";
22
21
  import {
@@ -224,7 +223,6 @@ export function PropertyPanelFlat({
224
223
  // Match the legacy Motion gate while preserving TypeScript narrowing.
225
224
  const showMotionTiming = Boolean(sections.timing);
226
225
  const gsapEffectHandlers =
227
- STUDIO_GSAP_PANEL_ENABLED &&
228
226
  onUpdateGsapProperty &&
229
227
  onUpdateGsapMeta &&
230
228
  onDeleteGsapAnimation &&
@@ -1,5 +1,24 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { resolveFloatingPanelPosition } from "./floatingPanel";
2
+ import { clampCentredLeft, resolveFloatingPanelPosition } from "./floatingPanel";
3
+
4
+ describe("clampCentredLeft", () => {
5
+ it("leaves a bubble that already fits alone", () => {
6
+ expect(clampCentredLeft(400, 104, 800, 8)).toBe(400);
7
+ });
8
+
9
+ it("pushes a bubble whose left half would leave the viewport", () => {
10
+ // Trigger centred at x=20 with a 104px bubble would render at left=-32.
11
+ expect(clampCentredLeft(20, 104, 800, 8)).toBe(60);
12
+ });
13
+
14
+ it("pushes a bubble whose right half would leave the viewport", () => {
15
+ expect(clampCentredLeft(790, 104, 800, 8)).toBe(740);
16
+ });
17
+
18
+ it("keeps the left edge visible when the bubble is wider than the viewport", () => {
19
+ expect(clampCentredLeft(10, 900, 800, 8)).toBe(458);
20
+ });
21
+ });
3
22
 
4
23
  describe("resolveFloatingPanelPosition", () => {
5
24
  it("places the panel below the anchor when there is space", () => {
@@ -22,6 +22,21 @@ function clamp(value: number, min: number, max: number): number {
22
22
  return Math.max(min, Math.min(max, value));
23
23
  }
24
24
 
25
+ /**
26
+ * Clamp the centre point of a centred bubble so the whole bubble stays in the
27
+ * viewport: clamping the centre alone lets a wide bubble hang off the edge.
28
+ */
29
+ export function clampCentredLeft(
30
+ centreX: number,
31
+ bubbleWidth: number,
32
+ viewportWidth: number,
33
+ margin: number,
34
+ ): number {
35
+ const half = bubbleWidth / 2;
36
+ const min = half + margin;
37
+ return clamp(centreX, min, Math.max(min, viewportWidth - half - margin));
38
+ }
39
+
25
40
  export function resolveFloatingPanelPosition(
26
41
  anchor: FloatingRect,
27
42
  viewport: FloatingSize,
@@ -16,35 +16,18 @@ describe("manual editing availability", () => {
16
16
  vi.resetModules();
17
17
  });
18
18
 
19
- it("enables inspector selection and manual dragging by default", async () => {
20
- const availability = await loadAvailabilityWithEnv({});
21
-
22
- expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(true);
23
- expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true);
24
- expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
25
- });
26
-
27
- it("disables preview selection when the inspector panel flag is explicitly off", async () => {
28
- const availability = await loadAvailabilityWithEnv({
29
- VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "0",
30
- });
31
-
32
- expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(false);
33
- expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(false);
34
- });
35
-
36
19
  it("enables feature flags with explicit truthy env values", () => {
37
20
  expect(
38
21
  resolveStudioBooleanEnvFlag(
39
- { VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING: "true" },
40
- ["VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING"],
22
+ { VITE_STUDIO_ENABLE_FLAT_INSPECTOR: "true" },
23
+ ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR"],
41
24
  false,
42
25
  ),
43
26
  ).toBe(true);
44
27
  expect(
45
28
  resolveStudioBooleanEnvFlag(
46
- { VITE_STUDIO_ENABLE_MOTION_PANEL: "1" },
47
- ["VITE_STUDIO_ENABLE_MOTION_PANEL"],
29
+ { VITE_STUDIO_SDK_CUTOVER_ENABLED: "1" },
30
+ ["VITE_STUDIO_SDK_CUTOVER_ENABLED"],
48
31
  false,
49
32
  ),
50
33
  ).toBe(true);
@@ -53,15 +36,15 @@ describe("manual editing availability", () => {
53
36
  it("disables feature flags with explicit falsy env values", () => {
54
37
  expect(
55
38
  resolveStudioBooleanEnvFlag(
56
- { VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING: "off" },
57
- ["VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING"],
39
+ { VITE_STUDIO_ENABLE_FLAT_INSPECTOR: "off" },
40
+ ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR"],
58
41
  true,
59
42
  ),
60
43
  ).toBe(false);
61
44
  expect(
62
45
  resolveStudioBooleanEnvFlag(
63
- { VITE_STUDIO_ENABLE_MOTION_PANEL: "0" },
64
- ["VITE_STUDIO_ENABLE_MOTION_PANEL"],
46
+ { VITE_STUDIO_SDK_CUTOVER_ENABLED: "0" },
47
+ ["VITE_STUDIO_SDK_CUTOVER_ENABLED"],
65
48
  true,
66
49
  ),
67
50
  ).toBe(false);
@@ -70,18 +53,8 @@ describe("manual editing availability", () => {
70
53
  it("supports legacy flag aliases after the preferred name", () => {
71
54
  expect(
72
55
  resolveStudioBooleanEnvFlag(
73
- { VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED: "yes" },
74
- [
75
- "VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING",
76
- "VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED",
77
- ],
78
- false,
79
- ),
80
- ).toBe(true);
81
- expect(
82
- resolveStudioBooleanEnvFlag(
83
- { VITE_STUDIO_MOTION_PANEL_ENABLED: "enabled" },
84
- ["VITE_STUDIO_ENABLE_MOTION_PANEL", "VITE_STUDIO_MOTION_PANEL_ENABLED"],
56
+ { VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "yes" },
57
+ ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
85
58
  false,
86
59
  ),
87
60
  ).toBe(true);
@@ -91,10 +64,10 @@ describe("manual editing availability", () => {
91
64
  expect(
92
65
  resolveStudioBooleanEnvFlag(
93
66
  {
94
- VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "off",
95
- VITE_STUDIO_INSPECTOR_PANELS_ENABLED: "on",
67
+ VITE_STUDIO_ENABLE_FLAT_INSPECTOR: "off",
68
+ VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "on",
96
69
  },
97
- ["VITE_STUDIO_ENABLE_INSPECTOR_PANELS", "VITE_STUDIO_INSPECTOR_PANELS_ENABLED"],
70
+ ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
98
71
  true,
99
72
  ),
100
73
  ).toBe(false);
@@ -1,7 +1,5 @@
1
1
  export type StudioFeatureFlagEnv = Record<string, boolean | string | undefined>;
2
2
 
3
- const STUDIO_PREVIEW_MANUAL_DRAGGING_ENV = "VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING";
4
- const STUDIO_INSPECTOR_PANELS_ENV = "VITE_STUDIO_ENABLE_INSPECTOR_PANELS";
5
3
  const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on", "enabled"]);
6
4
  const FALSY_ENV_VALUES = new Set(["0", "false", "no", "off", "disabled"]);
7
5
 
@@ -40,44 +38,6 @@ const runtimeEnv =
40
38
  : {};
41
39
  const env = { ...(import.meta.env ?? {}), ...runtimeEnv } as StudioFeatureFlagEnv;
42
40
 
43
- export const STUDIO_PREVIEW_MANUAL_EDITING_ENABLED = resolveStudioBooleanEnvFlag(
44
- env,
45
- [STUDIO_PREVIEW_MANUAL_DRAGGING_ENV, "VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED"],
46
- true,
47
- );
48
-
49
- export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag(
50
- env,
51
- [STUDIO_INSPECTOR_PANELS_ENV, "VITE_STUDIO_INSPECTOR_PANELS_ENABLED"],
52
- true,
53
- );
54
-
55
- export const STUDIO_BLOCKS_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
56
- env,
57
- ["VITE_STUDIO_ENABLE_BLOCKS_PANEL", "VITE_STUDIO_BLOCKS_PANEL_ENABLED"],
58
- true,
59
- );
60
-
61
- export const STUDIO_GSAP_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
62
- env,
63
- ["VITE_STUDIO_ENABLE_GSAP_PANEL", "VITE_STUDIO_GSAP_PANEL_ENABLED"],
64
- true,
65
- );
66
-
67
- export const STUDIO_KEYFRAMES_ENABLED = resolveStudioBooleanEnvFlag(
68
- env,
69
- ["VITE_STUDIO_ENABLE_KEYFRAMES", "VITE_STUDIO_KEYFRAMES_ENABLED"],
70
- true,
71
- );
72
-
73
- export const STUDIO_RAZOR_TOOL_ENABLED = resolveStudioBooleanEnvFlag(
74
- env,
75
- ["VITE_STUDIO_ENABLE_RAZOR_TOOL", "VITE_STUDIO_RAZOR_TOOL_ENABLED"],
76
- true,
77
- );
78
-
79
- export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
80
-
81
41
  // Stage 7 Step 3c: SDK cutover — routes inline-style ops through SDK dispatch
82
42
  // instead of the server patch-element API. Default false; enable via
83
43
  // VITE_STUDIO_SDK_CUTOVER_ENABLED=true. Requires SDK session to be open.
@@ -114,5 +74,4 @@ export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
114
74
  true,
115
75
  );
116
76
 
117
- export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
118
77
  import { resolveEnabledSdkFamilies } from "../../utils/sdkCutoverPolicy";
@@ -1,6 +1,5 @@
1
1
  import { useState } from "react";
2
2
  import type { DomEditSelection } from "./domEditingTypes";
3
- import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
4
3
  import { MetricField } from "./propertyPanelPrimitives";
5
4
  import { KeyframeNavigation } from "./KeyframeNavigation";
6
5
  import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
@@ -259,7 +258,7 @@ function Transform3dField({
259
258
  }}
260
259
  />
261
260
  </div>
262
- {STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
261
+ {(gsapAnimId || onCommitAnimatedProperty) && (
263
262
  <KeyframeNavigation
264
263
  property={prop}
265
264
  keyframes={ctx.gsapKeyframes}
@@ -2,7 +2,6 @@ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
2
2
  import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives";
3
3
  import { KeyframeNavigation } from "./KeyframeNavigation";
4
4
  import { formatPxMetricValue } from "./propertyPanelHelpers";
5
- import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
6
5
  import { resolveValueTier } from "./propertyPanelValueTier";
7
6
  import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
8
7
  import type { DomEditSelection } from "./domEditingTypes";
@@ -69,7 +68,7 @@ function KeyframeGutter({
69
68
  | "onConvertToKeyframes"
70
69
  >) {
71
70
  const track = useTrackDesignInput();
72
- if (!STUDIO_KEYFRAMES_ENABLED || !gsapAnimId) return null;
71
+ if (!gsapAnimId) return null;
73
72
  const hasKeyframesOnProp = Boolean(navKeyframes?.some((kf) => property in kf.properties));
74
73
  return (
75
74
  <span data-flat-kf-gutter="true" style={{ opacity: hasKeyframesOnProp ? 1 : 0.3 }}>
@@ -4,12 +4,6 @@ import { DomEditOverlay } from "../editor/DomEditOverlay";
4
4
  import { MotionPathOverlay } from "../editor/MotionPathOverlay";
5
5
  import { SnapToolbar } from "../editor/SnapToolbar";
6
6
  import { useCompositionDimensions } from "../../hooks/useCompositionDimensions";
7
- import {
8
- STUDIO_INSPECTOR_PANELS_ENABLED,
9
- STUDIO_KEYFRAMES_ENABLED,
10
- STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
11
- STUDIO_PREVIEW_SELECTION_ENABLED,
12
- } from "../editor/manualEditingAvailability";
13
7
  import { useStudioPlaybackContext, useStudioShellContext } from "../../contexts/StudioContext";
14
8
  import {
15
9
  useDomEditActionsContext,
@@ -203,21 +197,17 @@ export function PreviewOverlays({
203
197
  return <CaptionOverlay iframeRef={previewIframeRef} />;
204
198
  }
205
199
 
206
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) return null;
207
-
208
200
  return (
209
201
  <>
210
202
  <DomEditOverlay
211
203
  iframeRef={previewIframeRef}
212
204
  activeCompositionPath={activeCompPath}
213
205
  hoverSelection={
214
- STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode && !compositionLoading && !isPlaying
215
- ? domEditHoverSelection
216
- : null
206
+ !captionEditMode && !compositionLoading && !isPlaying ? domEditHoverSelection : null
217
207
  }
218
208
  selection={shouldShowSelectedDomBounds ? domEditSelection : null}
219
209
  groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
220
- allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED && !isGestureRecording}
210
+ allowCanvasMovement={!isGestureRecording}
221
211
  onCanvasMouseDown={handlePreviewCanvasMouseDown}
222
212
  onCanvasPointerMove={handlePreviewCanvasPointerMove}
223
213
  onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
@@ -273,14 +263,12 @@ export function PreviewOverlays({
273
263
  onMarqueeSelect={applyMarqueeSelection}
274
264
  />
275
265
  <SnapToolbar onSnapChange={setSnapPrefs} />
276
- {STUDIO_KEYFRAMES_ENABLED && (
277
- <MotionPathOverlay
278
- iframeRef={previewIframeRef}
279
- selection={shouldShowMotionPath ? domEditSelection : null}
280
- compositionSize={compositionDimensions}
281
- isPlaying={isPlaying}
282
- />
283
- )}
266
+ <MotionPathOverlay
267
+ iframeRef={previewIframeRef}
268
+ selection={shouldShowMotionPath ? domEditSelection : null}
269
+ compositionSize={compositionDimensions}
270
+ isPlaying={isPlaying}
271
+ />
284
272
  {gestureOverlay}
285
273
  </>
286
274
  );
@@ -0,0 +1,62 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { RenderQueue } from "./RenderQueue";
7
+
8
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
9
+
10
+ let root: Root | null = null;
11
+
12
+ afterEach(() => {
13
+ if (root) act(() => root?.unmount());
14
+ root = null;
15
+ document.body.innerHTML = "";
16
+ });
17
+
18
+ function mountRenderQueue(onStartRender: ReturnType<typeof vi.fn>) {
19
+ const host = document.createElement("div");
20
+ document.body.append(host);
21
+ root = createRoot(host);
22
+ act(() => {
23
+ root?.render(
24
+ <RenderQueue
25
+ jobs={[]}
26
+ projectId="demo"
27
+ onDelete={vi.fn()}
28
+ onClearCompleted={vi.fn()}
29
+ onStartRender={onStartRender}
30
+ isRendering={false}
31
+ compositionDimensions={{ width: 1920, height: 1080 }}
32
+ />,
33
+ );
34
+ });
35
+ return host;
36
+ }
37
+
38
+ describe("RenderQueue resolution submission", () => {
39
+ it("submits the canonical landscape 4K preset selected by the user", () => {
40
+ const onStartRender = vi.fn();
41
+ const host = mountRenderQueue(onStartRender);
42
+ const resolutionSelect = [...host.querySelectorAll("select")].find((select) =>
43
+ [...select.options].some((option) => option.textContent?.startsWith("4K")),
44
+ );
45
+ if (!resolutionSelect) throw new Error("resolution selector did not render");
46
+
47
+ act(() => {
48
+ resolutionSelect.value = "4k";
49
+ resolutionSelect.dispatchEvent(new Event("change", { bubbles: true }));
50
+ });
51
+
52
+ const exportButton = [...host.querySelectorAll("button")].find(
53
+ (button) => button.textContent === "Export",
54
+ );
55
+ if (!exportButton) throw new Error("export button did not render");
56
+ act(() => {
57
+ exportButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
58
+ });
59
+
60
+ expect(onStartRender).toHaveBeenCalledWith("mp4", "standard", "landscape-4k", 30);
61
+ });
62
+ });