@hyperframes/studio 0.6.107 → 0.6.109

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 (37) hide show
  1. package/dist/assets/{hyperframes-player-DgsMQSvV.js → hyperframes-player-67pq7USK.js} +2 -2
  2. package/dist/assets/index-BVqybwMG.css +1 -0
  3. package/dist/assets/{index-zIK7PWvk.js → index-CFCe0Xvn.js} +1 -1
  4. package/dist/assets/{index-P52mbTRb.js → index-SlMJloLR.js} +1 -1
  5. package/dist/assets/index-ho_f4axK.js +296 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +5 -5
  8. package/src/App.tsx +1 -0
  9. package/src/components/StudioRightPanel.tsx +161 -75
  10. package/src/components/editor/PropertyPanel.tsx +26 -1
  11. package/src/components/editor/domEditOverlayGeometry.ts +2 -11
  12. package/src/components/editor/domEditingDom.ts +21 -0
  13. package/src/components/editor/domEditingElement.ts +2 -11
  14. package/src/components/editor/manualEditingAvailability.test.ts +12 -0
  15. package/src/components/editor/manualEditingAvailability.ts +6 -0
  16. package/src/components/editor/propertyPanelColorGradingControls.tsx +475 -0
  17. package/src/components/editor/propertyPanelColorGradingSection.tsx +355 -0
  18. package/src/components/editor/propertyPanelHelpers.ts +2 -1
  19. package/src/components/editor/propertyPanelPrimitives.tsx +32 -37
  20. package/src/contexts/DomEditContext.tsx +4 -0
  21. package/src/contexts/PanelLayoutContext.tsx +6 -0
  22. package/src/hooks/useDomEditCommits.ts +7 -0
  23. package/src/hooks/useDomEditSession.ts +2 -2
  24. package/src/hooks/useDomEditTextCommits.ts +82 -26
  25. package/src/hooks/useDomEditWiring.ts +1 -0
  26. package/src/hooks/useDomSelection.ts +2 -10
  27. package/src/hooks/usePanelLayout.ts +26 -1
  28. package/src/hooks/useStudioContextValue.ts +8 -3
  29. package/src/icons/SystemIcons.tsx +4 -0
  30. package/src/player/lib/timelineDOM.test.ts +36 -0
  31. package/src/player/lib/timelineDOM.ts +2 -0
  32. package/src/player/lib/timelineElementHelpers.ts +18 -1
  33. package/src/styles/studio.css +81 -0
  34. package/src/utils/mediaTypes.ts +1 -0
  35. package/src/utils/studioHelpers.ts +6 -0
  36. package/dist/assets/index-BrhJl2JY.css +0 -1
  37. package/dist/assets/index-CVHV-S-B.js +0 -296
package/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-CVHV-S-B.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-BrhJl2JY.css">
8
+ <script type="module" crossorigin src="/assets/index-ho_f4axK.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BVqybwMG.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.107",
3
+ "version": "0.6.109",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,9 +33,9 @@
33
33
  "@phosphor-icons/react": "^2.1.10",
34
34
  "bpm-detective": "^2.0.5",
35
35
  "mediabunny": "^1.45.3",
36
- "@hyperframes/core": "0.6.107",
37
- "@hyperframes/player": "0.6.107",
38
- "@hyperframes/sdk": "0.6.107"
36
+ "@hyperframes/core": "0.6.109",
37
+ "@hyperframes/player": "0.6.109",
38
+ "@hyperframes/sdk": "0.6.109"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/react": "19",
@@ -49,7 +49,7 @@
49
49
  "vite": "^6.4.2",
50
50
  "vitest": "^3.2.4",
51
51
  "zustand": "^5.0.0",
52
- "@hyperframes/producer": "0.6.107"
52
+ "@hyperframes/producer": "0.6.109"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "react": "19",
package/src/App.tsx CHANGED
@@ -409,6 +409,7 @@ export function StudioApp() {
409
409
  shouldShowSelectedDomBounds,
410
410
  } = useInspectorState(
411
411
  panelLayout.rightPanelTab,
412
+ panelLayout.rightInspectorPanes,
412
413
  panelLayout.rightCollapsed,
413
414
  isPlaying,
414
415
  gestureState === "recording",
@@ -1,3 +1,4 @@
1
+ import { useCallback, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
1
2
  import { Tooltip } from "./ui";
2
3
  import { PropertyPanel } from "./editor/PropertyPanel";
3
4
  import { LayersPanel } from "./editor/LayersPanel";
@@ -14,6 +15,9 @@ import { useFileManagerContext } from "../contexts/FileManagerContext";
14
15
  import { useDomEditContext } from "../contexts/DomEditContext";
15
16
  import { usePlayerStore } from "../player";
16
17
 
18
+ const MIN_INSPECTOR_SPLIT_PERCENT = 20;
19
+ const MAX_INSPECTOR_SPLIT_PERCENT = 75;
20
+
17
21
  export interface StudioRightPanelProps {
18
22
  designPanelActive: boolean;
19
23
  activeBlockParams?: {
@@ -41,6 +45,8 @@ export function StudioRightPanel({
41
45
  rightWidth,
42
46
  rightPanelTab,
43
47
  setRightPanelTab,
48
+ rightInspectorPanes,
49
+ toggleRightInspectorPane,
44
50
  handlePanelResizeStart,
45
51
  handlePanelResizeMove,
46
52
  handlePanelResizeEnd,
@@ -63,6 +69,7 @@ export function StudioRightPanel({
63
69
  clearDomSelection,
64
70
  handleDomStyleCommit,
65
71
  handleDomAttributeCommit,
72
+ handleDomAttributeLiveCommit,
66
73
  handleDomHtmlAttributeCommit,
67
74
  handleDomPathOffsetCommit,
68
75
  handleDomBoxSizeCommit,
@@ -96,7 +103,130 @@ export function StudioRightPanel({
96
103
  const { assets, fontAssets, projectDir, handleImportFiles, handleImportFonts } =
97
104
  useFileManagerContext();
98
105
 
106
+ const [layersPanePercent, setLayersPanePercent] = useState(40);
107
+ const splitContainerRef = useRef<HTMLDivElement>(null);
108
+ const splitDragRef = useRef<{
109
+ startY: number;
110
+ startPercent: number;
111
+ height: number;
112
+ } | null>(null);
113
+
99
114
  const renderJobs = renderQueue.jobs as RenderJob[];
115
+ const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
116
+ const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
117
+ const layersPaneOpen =
118
+ inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;
119
+
120
+ const handleInspectorPaneButtonClick = (pane: "design" | "layers") => {
121
+ if (!inspectorTabActive) {
122
+ setRightPanelTab(pane);
123
+ return;
124
+ }
125
+ toggleRightInspectorPane(pane);
126
+ };
127
+
128
+ const handleInspectorSplitResizeStart = useCallback(
129
+ (event: ReactPointerEvent<HTMLDivElement>) => {
130
+ event.preventDefault();
131
+ event.currentTarget.setPointerCapture(event.pointerId);
132
+ const height = splitContainerRef.current?.getBoundingClientRect().height ?? 0;
133
+ splitDragRef.current = {
134
+ startY: event.clientY,
135
+ startPercent: layersPanePercent,
136
+ height,
137
+ };
138
+ },
139
+ [layersPanePercent],
140
+ );
141
+
142
+ const handleInspectorSplitResizeMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
143
+ const drag = splitDragRef.current;
144
+ if (!drag || drag.height <= 0) return;
145
+ const deltaPercent = ((event.clientY - drag.startY) / drag.height) * 100;
146
+ const next = Math.min(
147
+ MAX_INSPECTOR_SPLIT_PERCENT,
148
+ Math.max(MIN_INSPECTOR_SPLIT_PERCENT, drag.startPercent + deltaPercent),
149
+ );
150
+ setLayersPanePercent(next);
151
+ }, []);
152
+
153
+ const handleInspectorSplitResizeEnd = useCallback(() => {
154
+ splitDragRef.current = null;
155
+ }, []);
156
+
157
+ const propertyPanel = (
158
+ <PropertyPanel
159
+ projectId={projectId}
160
+ projectDir={projectDir}
161
+ assets={assets}
162
+ element={domEditGroupSelections.length > 1 ? null : domEditSelection}
163
+ multiSelectCount={domEditGroupSelections.length}
164
+ copiedAgentPrompt={copiedAgentPrompt}
165
+ onClearSelection={clearDomSelection}
166
+ onSetStyle={handleDomStyleCommit}
167
+ onSetAttribute={handleDomAttributeCommit}
168
+ onSetAttributeLive={handleDomAttributeLiveCommit}
169
+ onSetHtmlAttribute={handleDomHtmlAttributeCommit}
170
+ onSetManualOffset={handleDomPathOffsetCommit}
171
+ onSetManualSize={handleDomBoxSizeCommit}
172
+ onSetManualRotation={handleDomRotationCommit}
173
+ onSetText={handleDomTextCommit}
174
+ onSetTextFieldStyle={handleDomTextFieldStyleCommit}
175
+ onAddTextField={handleDomAddTextField}
176
+ onRemoveTextField={handleDomRemoveTextField}
177
+ onAskAgent={handleAskAgent}
178
+ onImportAssets={handleImportFiles}
179
+ fontAssets={fontAssets}
180
+ onImportFonts={handleImportFonts}
181
+ previewIframeRef={previewIframeRef}
182
+ gsapAnimations={selectedGsapAnimations}
183
+ gsapMultipleTimelines={gsapMultipleTimelines}
184
+ gsapUnsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
185
+ onUpdateGsapProperty={handleGsapUpdateProperty}
186
+ onUpdateGsapMeta={handleGsapUpdateMeta}
187
+ onDeleteGsapAnimation={handleGsapDeleteAnimation}
188
+ onAddGsapProperty={handleGsapAddProperty}
189
+ onRemoveGsapProperty={handleGsapRemoveProperty}
190
+ onUpdateGsapFromProperty={handleGsapUpdateFromProperty}
191
+ onAddGsapFromProperty={handleGsapAddFromProperty}
192
+ onRemoveGsapFromProperty={handleGsapRemoveFromProperty}
193
+ onAddGsapAnimation={handleGsapAddAnimation}
194
+ onCommitAnimatedProperty={commitAnimatedProperty}
195
+ onAddKeyframe={handleGsapAddKeyframe}
196
+ onRemoveKeyframe={handleGsapRemoveKeyframe}
197
+ onConvertToKeyframes={handleGsapConvertToKeyframes}
198
+ onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)}
199
+ onSetArcPath={handleSetArcPath}
200
+ onUpdateArcSegment={handleUpdateArcSegment}
201
+ onUnroll={handleUnroll}
202
+ recordingState={recordingState}
203
+ recordingDuration={recordingDuration}
204
+ onToggleRecording={onToggleRecording}
205
+ />
206
+ );
207
+
208
+ const renderQueuePanel = (
209
+ <RenderQueue
210
+ jobs={renderJobs}
211
+ projectId={projectId}
212
+ onDelete={renderQueue.deleteRender}
213
+ onClearCompleted={renderQueue.clearCompleted}
214
+ onStartRender={async (format, quality, resolution, fps) => {
215
+ await waitForPendingDomEditSaves();
216
+ const composition =
217
+ activeCompPath && activeCompPath !== "index.html" ? activeCompPath : undefined;
218
+ await renderQueue.startRender({
219
+ fps,
220
+ quality,
221
+ format,
222
+ resolution,
223
+ composition,
224
+ });
225
+ }}
226
+ compositionDimensions={compositionDimensions}
227
+ isRendering={renderQueue.isRendering}
228
+ />
229
+ );
100
230
 
101
231
  return (
102
232
  <>
@@ -123,9 +253,9 @@ export function StudioRightPanel({
123
253
  <Tooltip label="Element styles and properties" side="bottom">
124
254
  <button
125
255
  type="button"
126
- onClick={() => setRightPanelTab("design")}
256
+ onClick={() => handleInspectorPaneButtonClick("design")}
127
257
  className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
128
- rightPanelTab === "design"
258
+ designPaneOpen
129
259
  ? "bg-neutral-800 text-white"
130
260
  : "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
131
261
  }`}
@@ -136,9 +266,9 @@ export function StudioRightPanel({
136
266
  <Tooltip label="Composition layer stack" side="bottom">
137
267
  <button
138
268
  type="button"
139
- onClick={() => setRightPanelTab("layers")}
269
+ onClick={() => handleInspectorPaneButtonClick("layers")}
140
270
  className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors ${
141
- rightPanelTab === "layers"
271
+ layersPaneOpen
142
272
  ? "bg-neutral-800 text-white"
143
273
  : "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
144
274
  }`}
@@ -171,79 +301,35 @@ export function StudioRightPanel({
171
301
  compositionPath={activeBlockParams.compositionPath}
172
302
  onClose={onCloseBlockParams ?? (() => {})}
173
303
  />
174
- ) : rightPanelTab === "layers" ? (
304
+ ) : layersPaneOpen && designPaneOpen ? (
305
+ <div ref={splitContainerRef} className="flex h-full min-h-0 flex-col">
306
+ <div
307
+ className="min-h-[120px] overflow-hidden"
308
+ style={{ flexBasis: `${layersPanePercent}%`, flexShrink: 0 }}
309
+ >
310
+ <LayersPanel />
311
+ </div>
312
+ <div
313
+ role="separator"
314
+ aria-label="Resize Layers and Design panes"
315
+ aria-orientation="horizontal"
316
+ className="group flex h-2 flex-shrink-0 cursor-row-resize items-center justify-center border-y border-neutral-800 bg-neutral-900"
317
+ style={{ touchAction: "none" }}
318
+ onPointerDown={handleInspectorSplitResizeStart}
319
+ onPointerMove={handleInspectorSplitResizeMove}
320
+ onPointerUp={handleInspectorSplitResizeEnd}
321
+ onPointerCancel={handleInspectorSplitResizeEnd}
322
+ >
323
+ <div className="h-px w-10 rounded-full bg-white/12 transition-colors group-hover:bg-white/24 group-active:bg-studio-accent/70" />
324
+ </div>
325
+ <div className="min-h-0 flex-1 overflow-hidden">{propertyPanel}</div>
326
+ </div>
327
+ ) : layersPaneOpen ? (
175
328
  <LayersPanel />
176
- ) : designPanelActive ? (
177
- <PropertyPanel
178
- projectId={projectId}
179
- projectDir={projectDir}
180
- assets={assets}
181
- element={domEditGroupSelections.length > 1 ? null : domEditSelection}
182
- multiSelectCount={domEditGroupSelections.length}
183
- copiedAgentPrompt={copiedAgentPrompt}
184
- onClearSelection={clearDomSelection}
185
- onSetStyle={handleDomStyleCommit}
186
- onSetAttribute={handleDomAttributeCommit}
187
- onSetHtmlAttribute={handleDomHtmlAttributeCommit}
188
- onSetManualOffset={handleDomPathOffsetCommit}
189
- onSetManualSize={handleDomBoxSizeCommit}
190
- onSetManualRotation={handleDomRotationCommit}
191
- onSetText={handleDomTextCommit}
192
- onSetTextFieldStyle={handleDomTextFieldStyleCommit}
193
- onAddTextField={handleDomAddTextField}
194
- onRemoveTextField={handleDomRemoveTextField}
195
- onAskAgent={handleAskAgent}
196
- onImportAssets={handleImportFiles}
197
- fontAssets={fontAssets}
198
- onImportFonts={handleImportFonts}
199
- previewIframeRef={previewIframeRef}
200
- gsapAnimations={selectedGsapAnimations}
201
- gsapMultipleTimelines={gsapMultipleTimelines}
202
- gsapUnsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
203
- onUpdateGsapProperty={handleGsapUpdateProperty}
204
- onUpdateGsapMeta={handleGsapUpdateMeta}
205
- onDeleteGsapAnimation={handleGsapDeleteAnimation}
206
- onAddGsapProperty={handleGsapAddProperty}
207
- onRemoveGsapProperty={handleGsapRemoveProperty}
208
- onUpdateGsapFromProperty={handleGsapUpdateFromProperty}
209
- onAddGsapFromProperty={handleGsapAddFromProperty}
210
- onRemoveGsapFromProperty={handleGsapRemoveFromProperty}
211
- onAddGsapAnimation={handleGsapAddAnimation}
212
- onCommitAnimatedProperty={commitAnimatedProperty}
213
- onAddKeyframe={handleGsapAddKeyframe}
214
- onRemoveKeyframe={handleGsapRemoveKeyframe}
215
- onConvertToKeyframes={handleGsapConvertToKeyframes}
216
- onSeekToTime={(t) => usePlayerStore.getState().requestSeek(t)}
217
- onSetArcPath={handleSetArcPath}
218
- onUpdateArcSegment={handleUpdateArcSegment}
219
- onUnroll={handleUnroll}
220
- recordingState={recordingState}
221
- recordingDuration={recordingDuration}
222
- onToggleRecording={onToggleRecording}
223
- />
329
+ ) : designPaneOpen ? (
330
+ propertyPanel
224
331
  ) : (
225
- <RenderQueue
226
- jobs={renderJobs}
227
- projectId={projectId}
228
- onDelete={renderQueue.deleteRender}
229
- onClearCompleted={renderQueue.clearCompleted}
230
- onStartRender={async (format, quality, resolution, fps) => {
231
- await waitForPendingDomEditSaves();
232
- const composition =
233
- activeCompPath && activeCompPath !== "index.html"
234
- ? activeCompPath
235
- : undefined;
236
- await renderQueue.startRender({
237
- fps,
238
- quality,
239
- format,
240
- resolution,
241
- composition,
242
- });
243
- }}
244
- compositionDimensions={compositionDimensions}
245
- isRendering={renderQueue.isRendering}
246
- />
332
+ renderQueuePanel
247
333
  )}
248
334
  </div>
249
335
  </>
@@ -14,11 +14,19 @@ import { MetricField, Section } from "./propertyPanelPrimitives";
14
14
  import { createTransformCommitHandlers } from "./propertyPanelTransformCommit";
15
15
  import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
16
16
  import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
17
+ import {
18
+ ColorGradingSection,
19
+ isColorGradingCapableElement,
20
+ } from "./propertyPanelColorGradingSection";
17
21
  import { TextSection, StyleSections } from "./propertyPanelSections";
18
22
  import { GsapAnimationSection } from "./GsapAnimationSection";
19
23
  import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
20
24
  import { KeyframeNavigation } from "./KeyframeNavigation";
21
- import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
25
+ import {
26
+ STUDIO_COLOR_GRADING_ENABLED,
27
+ STUDIO_GSAP_PANEL_ENABLED,
28
+ STUDIO_KEYFRAMES_ENABLED,
29
+ } from "./manualEditingAvailability";
22
30
  import { usePlayerStore, liveTime } from "../../player";
23
31
  import { TimingSection } from "./propertyPanelTimingSection";
24
32
  import { type PropertyPanelProps } from "./propertyPanelHelpers";
@@ -47,6 +55,7 @@ export const PropertyPanel = memo(function PropertyPanel({
47
55
  onClearSelection,
48
56
  onSetStyle,
49
57
  onSetAttribute,
58
+ onSetAttributeLive,
50
59
  onSetHtmlAttribute,
51
60
  onSetManualOffset,
52
61
  onSetManualSize,
@@ -355,6 +364,22 @@ export const PropertyPanel = memo(function PropertyPanel({
355
364
  />
356
365
  )}
357
366
 
367
+ {STUDIO_COLOR_GRADING_ENABLED && isColorGradingCapableElement(element) && (
368
+ <ColorGradingSection
369
+ key={[
370
+ element.id ?? "",
371
+ element.hfId ?? "",
372
+ element.selector ?? "",
373
+ String(element.selectorIndex ?? ""),
374
+ ].join("|")}
375
+ element={element}
376
+ assets={assets}
377
+ previewIframeRef={previewIframeRef}
378
+ onImportAssets={onImportAssets}
379
+ onSetAttributeLive={onSetAttributeLive}
380
+ />
381
+ )}
382
+
358
383
  <Section title="Layout" icon={<Move size={15} />}>
359
384
  <div className={RESPONSIVE_GRID}>
360
385
  <div className="flex items-center gap-1">
@@ -1,4 +1,5 @@
1
1
  import { type DomEditSelection, findElementForSelection } from "./domEditing";
2
+ import { isElementVisibleThroughAncestors } from "./domEditingDom";
2
3
 
3
4
  export interface OverlayRect {
4
5
  left: number;
@@ -21,17 +22,7 @@ export type ResolvedElementRef = {
21
22
  };
22
23
 
23
24
  export function isElementVisibleForOverlay(el: HTMLElement): boolean {
24
- const win = el.ownerDocument.defaultView;
25
- if (!win) return true;
26
- let current: HTMLElement | null = el;
27
- while (current) {
28
- const computed = win.getComputedStyle(current);
29
- if (computed.display === "none" || computed.visibility === "hidden") return false;
30
- const opacity = Number.parseFloat(computed.opacity);
31
- if (Number.isFinite(opacity) && opacity <= 0.01) return false;
32
- current = current.parentElement;
33
- }
34
- return true;
25
+ return isElementVisibleThroughAncestors(el);
35
26
  }
36
27
 
37
28
  function readPositiveDimension(value: string | null): number | null {
@@ -57,6 +57,27 @@ export function isTextBearingTag(tagName: string): boolean {
57
57
  return ["div", "span", "p", "strong", "h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
58
58
  }
59
59
 
60
+ const COLOR_GRADING_SOURCE_HIDDEN_ATTR = "data-hf-color-grading-source-hidden";
61
+
62
+ export function isElementVisibleThroughAncestors(el: HTMLElement): boolean {
63
+ const win = el.ownerDocument.defaultView;
64
+ if (!win) return true;
65
+ let current: HTMLElement | null = el;
66
+ while (current) {
67
+ const computed = win.getComputedStyle(current);
68
+ if (computed.display === "none" || computed.visibility === "hidden") return false;
69
+ const opacity = Number.parseFloat(computed.opacity);
70
+ if (
71
+ Number.isFinite(opacity) &&
72
+ opacity <= 0.01 &&
73
+ !current.hasAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR)
74
+ )
75
+ return false;
76
+ current = current.parentElement;
77
+ }
78
+ return true;
79
+ }
80
+
60
81
  // ─── Style accessors ──────────────────────────────────────────────────────────
61
82
 
62
83
  export function getCuratedComputedStyles(el: HTMLElement): Record<string, string> {
@@ -15,6 +15,7 @@ import {
15
15
  getSelectorIndex,
16
16
  getSourceFileForElement,
17
17
  isHtmlElement,
18
+ isElementVisibleThroughAncestors,
18
19
  normalizeTimelineCompositionSource,
19
20
  querySelectorAllSafely,
20
21
  } from "./domEditingDom";
@@ -22,17 +23,7 @@ import {
22
23
  // ─── Visibility ──────────────────────────────────────────────────────────────
23
24
 
24
25
  export function isElementComputedVisible(el: HTMLElement): boolean {
25
- const win = el.ownerDocument.defaultView;
26
- if (!win) return true;
27
- let current: HTMLElement | null = el;
28
- while (current) {
29
- const computed = win.getComputedStyle(current);
30
- if (computed.display === "none" || computed.visibility === "hidden") return false;
31
- const opacity = Number.parseFloat(computed.opacity);
32
- if (Number.isFinite(opacity) && opacity <= 0.01) return false;
33
- current = current.parentElement;
34
- }
35
- return true;
26
+ return isElementVisibleThroughAncestors(el);
36
27
  }
37
28
 
38
29
  const VISUAL_LEAF_TAGS = new Set(["img", "video", "canvas", "svg", "audio"]);
@@ -29,6 +29,18 @@ describe("manual editing availability", () => {
29
29
  expect(availability.STUDIO_GSAP_DRAG_INTERCEPT_ENABLED).toBe(true);
30
30
  });
31
31
 
32
+ it("keeps color grading off by default", async () => {
33
+ const availability = await loadAvailabilityWithEnv({});
34
+ expect(availability.STUDIO_COLOR_GRADING_ENABLED).toBe(false);
35
+ });
36
+
37
+ it("enables color grading with an explicit env flag", async () => {
38
+ const availability = await loadAvailabilityWithEnv({
39
+ VITE_STUDIO_ENABLE_COLOR_GRADING: "1",
40
+ });
41
+ expect(availability.STUDIO_COLOR_GRADING_ENABLED).toBe(true);
42
+ });
43
+
32
44
  it("disables GSAP drag intercept when env var is false", async () => {
33
45
  const availability = await loadAvailabilityWithEnv({
34
46
  VITE_STUDIO_ENABLE_GSAP_DRAG_INTERCEPT: "false",
@@ -64,6 +64,12 @@ export const STUDIO_GSAP_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
64
64
  true,
65
65
  );
66
66
 
67
+ export const STUDIO_COLOR_GRADING_ENABLED = resolveStudioBooleanEnvFlag(
68
+ env,
69
+ ["VITE_STUDIO_ENABLE_COLOR_GRADING", "VITE_STUDIO_COLOR_GRADING_ENABLED"],
70
+ false,
71
+ );
72
+
67
73
  export const STUDIO_KEYFRAMES_ENABLED = resolveStudioBooleanEnvFlag(
68
74
  env,
69
75
  ["VITE_STUDIO_ENABLE_KEYFRAMES", "VITE_STUDIO_KEYFRAMES_ENABLED"],