@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.
- package/dist/assets/{hyperframes-player-B32VouCm.js → hyperframes-player-DSRuJMfh.js} +1 -1
- package/dist/assets/{index-DCTAKfpa.js → index-BrWVfpCQ.js} +1 -1
- package/dist/assets/{index-B5u2_pSh.js → index-DRtvHA1J.js} +150 -150
- package/dist/assets/{index-HeUw0EKI.js → index-YZR6OfQ5.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +3057 -3101
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +2 -5
- package/src/components/StudioHeader.tsx +3 -18
- package/src/components/StudioRightPanel.tsx +14 -22
- package/src/components/TimelineToolbar.tsx +120 -132
- package/src/components/editor/EaseCurveSection.tsx +9 -90
- package/src/components/editor/EaseModeControls.tsx +110 -0
- package/src/components/editor/PropertyPanel.test.tsx +2 -3
- package/src/components/editor/PropertyPanel.tsx +7 -12
- package/src/components/editor/PropertyPanelFlat.tsx +0 -2
- package/src/components/editor/floatingPanel.test.ts +20 -1
- package/src/components/editor/floatingPanel.ts +15 -0
- package/src/components/editor/manualEditingAvailability.test.ts +13 -40
- package/src/components/editor/manualEditingAvailability.ts +0 -41
- package/src/components/editor/propertyPanel3dTransform.tsx +1 -2
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +1 -2
- package/src/components/nle/PreviewOverlays.tsx +8 -20
- package/src/components/renders/RenderQueue.test.tsx +62 -0
- package/src/components/renders/RenderQueue.tsx +60 -38
- package/src/components/renders/useRenderQueue.ts +5 -11
- package/src/components/sidebar/LeftSidebar.tsx +15 -22
- package/src/components/ui/Tooltip.tsx +25 -7
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useDomEditPreviewSync.ts +1 -2
- package/src/hooks/useDomEditWiring.ts +2 -3
- package/src/hooks/useDomSelection.ts +0 -27
- package/src/hooks/usePreviewInteraction.ts +2 -3
- package/src/hooks/useStudioContextValue.ts +3 -7
- package/src/player/components/Timeline.tsx +1 -2
- package/src/player/components/TimelineLanes.tsx +33 -35
- package/src/player/components/useAutoExpandKeyframedClips.ts +0 -2
- package/src/player/components/useTimelineTrackLayout.ts +1 -5
- package/src/utils/studioUrlState.test.ts +5 -8
- 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
|
-
//
|
|
451
|
-
//
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
40
|
-
["
|
|
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
|
-
{
|
|
47
|
-
["
|
|
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
|
-
{
|
|
57
|
-
["
|
|
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
|
-
{
|
|
64
|
-
["
|
|
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
|
-
{
|
|
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
|
-
|
|
95
|
-
|
|
67
|
+
VITE_STUDIO_ENABLE_FLAT_INSPECTOR: "off",
|
|
68
|
+
VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "on",
|
|
96
69
|
},
|
|
97
|
-
["
|
|
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
|
-
{
|
|
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 (!
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
});
|