@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
@@ -1,6 +1,9 @@
1
- import { memo, useState, useRef, useEffect, useId } from "react";
1
+ import { memo, useState, useRef, useEffect, useLayoutEffect, useId } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { CANVAS_DIMENSIONS } from "@hyperframes/parsers";
2
4
  import { RenderQueueItem } from "./RenderQueueItem";
3
5
  import { Button } from "../ui/Button";
6
+ import { resolveFloatingPanelPosition, type FloatingPosition } from "../editor/floatingPanel";
4
7
  import type { RenderJob, ResolutionPreset } from "./useRenderQueue";
5
8
  import { getPersistedRenderSettings, persistRenderSettings } from "./renderSettings";
6
9
  import { trackStudioEvent } from "../../utils/studioTelemetry";
@@ -53,17 +56,6 @@ const SCALE_LABEL: Record<RenderScale, string> = {
53
56
  "4k": "4K",
54
57
  };
55
58
 
56
- // Mirrors `CANVAS_DIMENSIONS` in @hyperframes/core. Studio can't import from
57
- // the core barrel (it transitively pulls in node:fs) and the values are stable.
58
- const CANVAS_DIMENSIONS: Record<ResolutionPreset, CompositionDimensions> = {
59
- landscape: { width: 1920, height: 1080 },
60
- portrait: { width: 1080, height: 1920 },
61
- "landscape-4k": { width: 3840, height: 2160 },
62
- "portrait-4k": { width: 2160, height: 3840 },
63
- square: { width: 1080, height: 1080 },
64
- "square-4k": { width: 2160, height: 2160 },
65
- };
66
-
67
59
  type CompAspect = "landscape" | "portrait" | "square";
68
60
 
69
61
  function compAspect(dims: CompositionDimensions | null | undefined): CompAspect {
@@ -142,12 +134,20 @@ const FORMAT_INFO: Record<"mp4" | "webm" | "mov", { label: string; desc: string
142
134
  },
143
135
  };
144
136
 
137
+ // Estimated, like COLOR_PICKER_SIZE in propertyPanelColor: only the flip
138
+ // decision uses the height, and the clamp keeps the panel on screen either way.
139
+ const FORMAT_PANEL_SIZE = { width: 208, height: 150 };
140
+
145
141
  // Rich format guidance in a keyboard-reachable disclosure: the trigger is a
146
142
  // real button (focusable, labelled), the panel is tied to it via
147
143
  // aria-describedby, and Escape dismisses (WCAG 1.4.13). Content is too rich
148
144
  // for the one-line ui/Tooltip primitive, so this stays a local popover.
145
+ // It renders in a portal because the right panel is overflow-hidden: an
146
+ // in-flow absolute panel gets clipped at the panel edge.
149
147
  function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
150
148
  const [open, setOpen] = useState(false);
149
+ const [position, setPosition] = useState<FloatingPosition | null>(null);
150
+ const triggerRef = useRef<HTMLDivElement>(null);
151
151
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
152
152
  const panelId = useId();
153
153
 
@@ -161,6 +161,22 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
161
161
 
162
162
  useEffect(() => () => clearTimeout(timeoutRef.current), []);
163
163
 
164
+ // Positioned once on open, so it does not follow panel scroll. The popover
165
+ // is hover-lived; add a scroll listener only if that ever shows up.
166
+ useLayoutEffect(() => {
167
+ if (!open) return;
168
+ const el = triggerRef.current;
169
+ if (!el) return;
170
+ setPosition(
171
+ resolveFloatingPanelPosition(
172
+ el.getBoundingClientRect(),
173
+ { width: window.innerWidth, height: window.innerHeight },
174
+ FORMAT_PANEL_SIZE,
175
+ { offset: 6 },
176
+ ),
177
+ );
178
+ }, [open]);
179
+
164
180
  useEffect(() => {
165
181
  if (!open) return;
166
182
  const onKeyDown = (e: KeyboardEvent) => {
@@ -173,7 +189,7 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
173
189
  const info = FORMAT_INFO[format];
174
190
 
175
191
  return (
176
- <div className="relative" onPointerEnter={show} onPointerLeave={hide}>
192
+ <div ref={triggerRef} className="relative" onPointerEnter={show} onPointerLeave={hide}>
177
193
  <button
178
194
  type="button"
179
195
  aria-label="About video formats"
@@ -200,27 +216,32 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
200
216
  <line x1="12" y1="17" x2="12.01" y2="17" />
201
217
  </svg>
202
218
  </button>
203
- {open && (
204
- <div
205
- id={panelId}
206
- role="tooltip"
207
- className="absolute top-full right-0 mt-1.5 w-52 p-2 rounded bg-panel-input border border-neutral-700 shadow-lg z-50"
208
- >
209
- <p className="text-[10px] font-semibold text-panel-text-1 mb-0.5">{info.label}</p>
210
- <p className="text-[9px] text-panel-text-3 leading-tight">{info.desc}</p>
211
- <div className="mt-1.5 pt-1.5 border-t border-neutral-800">
212
- {(["mp4", "mov", "webm"] as const)
213
- .filter((f) => f !== format)
214
- .map((f) => (
215
- <p key={f} className="text-[9px] text-panel-text-4 leading-relaxed">
216
- <span className="text-panel-text-3 font-medium">{FORMAT_INFO[f].label}</span>
217
- {" "}
218
- {FORMAT_INFO[f].desc}
219
- </p>
220
- ))}
221
- </div>
222
- </div>
223
- )}
219
+ {open &&
220
+ createPortal(
221
+ <div
222
+ id={panelId}
223
+ role="tooltip"
224
+ onPointerEnter={show}
225
+ onPointerLeave={hide}
226
+ className="fixed w-52 p-2 rounded bg-panel-input border border-neutral-700 shadow-lg z-[200]"
227
+ style={{ left: position?.left ?? -9999, top: position?.top ?? -9999 }}
228
+ >
229
+ <p className="text-[10px] font-semibold text-panel-text-1 mb-0.5">{info.label}</p>
230
+ <p className="text-[9px] text-panel-text-3 leading-tight">{info.desc}</p>
231
+ <div className="mt-1.5 pt-1.5 border-t border-neutral-800">
232
+ {(["mp4", "mov", "webm"] as const)
233
+ .filter((f) => f !== format)
234
+ .map((f) => (
235
+ <p key={f} className="text-[9px] text-panel-text-4 leading-relaxed">
236
+ <span className="text-panel-text-3 font-medium">{FORMAT_INFO[f].label}</span>
237
+ {" — "}
238
+ {FORMAT_INFO[f].desc}
239
+ </p>
240
+ ))}
241
+ </div>
242
+ </div>,
243
+ document.body,
244
+ )}
224
245
  </div>
225
246
  );
226
247
  }
@@ -254,7 +275,7 @@ function FormatExportButton({
254
275
  const persisted = getPersistedRenderSettings();
255
276
  const [format, setFormat] = useState<"mp4" | "webm" | "mov">(persisted.format);
256
277
  const [quality, setQuality] = useState<"draft" | "standard" | "high">(persisted.quality);
257
- const [resolution, setResolution] = useState<ResolutionPreset | "auto">("auto");
278
+ const [resolution, setResolution] = useState<RenderScale>("auto");
258
279
  const [fps, setFps] = useState<24 | 30 | 60>(persisted.fps);
259
280
 
260
281
  // MOV (ProRes) is a fixed-quality codec — quality selector has no effect.
@@ -290,7 +311,7 @@ function FormatExportButton({
290
311
  <span className="text-[10px] text-panel-text-4">Resolution</span>
291
312
  <select
292
313
  value={resolution}
293
- onChange={(e) => setResolution(e.target.value as ResolutionPreset | "auto")}
314
+ onChange={(e) => setResolution(e.target.value as RenderScale)}
294
315
  disabled={isRendering}
295
316
  className={selectCls}
296
317
  >
@@ -352,8 +373,9 @@ function FormatExportButton({
352
373
  // loading already disables the button; this guard also stops a
353
374
  // double-click in the same frame from enqueueing two renders.
354
375
  if (isRendering) return;
355
- trackStudioEvent("render_start", { format, quality, resolution, fps });
356
- void onStartRender(format, quality, resolution, fps);
376
+ const outputResolution = resolveResolution(resolution, compositionDimensions);
377
+ trackStudioEvent("render_start", { format, quality, resolution: outputResolution, fps });
378
+ void onStartRender(format, quality, outputResolution, fps);
357
379
  }}
358
380
  className="w-full text-[11px] font-semibold"
359
381
  >
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect, useCallback, useRef, useMemo } from "react";
2
+ import type { CanvasResolution } from "@hyperframes/parsers";
2
3
  import { trackStudioRenderStart } from "../../telemetry/events";
3
4
  import { getAnonymousId } from "../../telemetry/config";
4
5
  import { generateId } from "../../utils/generateId";
@@ -14,17 +15,10 @@ export interface RenderJob {
14
15
  durationMs?: number;
15
16
  }
16
17
 
17
- // Mirrors `CanvasResolution` from @hyperframes/core. Kept local because
18
- // studio's tsconfig doesn't include node types, and the core barrel
19
- // transitively pulls in modules with `node:fs` imports. Drift risk is
20
- // low (6 string literals kept in sync manually with CANVAS_DIMENSIONS).
21
- export type ResolutionPreset =
22
- | "landscape"
23
- | "portrait"
24
- | "landscape-4k"
25
- | "portrait-4k"
26
- | "square"
27
- | "square-4k";
18
+ // The CLI consumes this same source through @hyperframes/core's re-export.
19
+ // Importing from the browser-safe parsers package avoids the core barrel's
20
+ // Node-only transitive modules without duplicating the preset union in Studio.
21
+ export type ResolutionPreset = CanvasResolution;
28
22
 
29
23
  export interface StartRenderOptions {
30
24
  fps?: number;
@@ -12,7 +12,6 @@ import { AssetsTab } from "./AssetsTab";
12
12
  import { trackStudioEvent } from "../../utils/studioTelemetry";
13
13
  import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab";
14
14
  import { FileTree } from "../editor/FileTree";
15
- import { STUDIO_BLOCKS_PANEL_ENABLED } from "../editor/manualEditingAvailability";
16
15
  import { Tooltip } from "../ui";
17
16
 
18
17
  export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
@@ -127,11 +126,7 @@ export const LeftSidebar = memo(
127
126
  <div className="flex items-center gap-2">
128
127
  <div
129
128
  className="grid min-w-0 flex-1 gap-0.5 rounded-[18px] bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
130
- style={{
131
- gridTemplateColumns: STUDIO_BLOCKS_PANEL_ENABLED
132
- ? "1fr 1fr 1fr 1fr"
133
- : "1fr 1fr 1fr",
134
- }}
129
+ style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}
135
130
  >
136
131
  <Tooltip label="Source code editor" side="bottom">
137
132
  <button
@@ -172,21 +167,19 @@ export const LeftSidebar = memo(
172
167
  Assets
173
168
  </button>
174
169
  </Tooltip>
175
- {STUDIO_BLOCKS_PANEL_ENABLED && (
176
- <Tooltip label="Browse blocks and components" side="bottom">
177
- <button
178
- type="button"
179
- onClick={() => selectTab("blocks")}
180
- className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
181
- tab === "blocks"
182
- ? "bg-neutral-800 text-white"
183
- : "text-neutral-500 hover:text-neutral-200"
184
- }`}
185
- >
186
- Catalog
187
- </button>
188
- </Tooltip>
189
- )}
170
+ <Tooltip label="Browse blocks and components" side="bottom">
171
+ <button
172
+ type="button"
173
+ onClick={() => selectTab("blocks")}
174
+ className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
175
+ tab === "blocks"
176
+ ? "bg-neutral-800 text-white"
177
+ : "text-neutral-500 hover:text-neutral-200"
178
+ }`}
179
+ >
180
+ Catalog
181
+ </button>
182
+ </Tooltip>
190
183
  </div>
191
184
  {onToggleCollapse && (
192
185
  <button
@@ -267,7 +260,7 @@ export const LeftSidebar = memo(
267
260
  </div>
268
261
  )}
269
262
 
270
- {STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && (
263
+ {tab === "blocks" && (
271
264
  <BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
272
265
  )}
273
266
 
@@ -1,5 +1,14 @@
1
- import { useState, useRef, useCallback, useEffect, useId, type ReactNode } from "react";
1
+ import {
2
+ useState,
3
+ useRef,
4
+ useCallback,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useId,
8
+ type ReactNode,
9
+ } from "react";
2
10
  import { createPortal } from "react-dom";
11
+ import { clampCentredLeft } from "../editor/floatingPanel";
3
12
 
4
13
  interface TooltipProps {
5
14
  label: string;
@@ -19,6 +28,8 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
19
28
  const [resolvedSide, setResolvedSide] = useState<"top" | "bottom">(side);
20
29
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
21
30
  const triggerRef = useRef<HTMLSpanElement>(null);
31
+ const bubbleRef = useRef<HTMLDivElement>(null);
32
+ const [bubbleWidth, setBubbleWidth] = useState(0);
22
33
  // WCAG 4.1.2: programmatically associate the bubble with its trigger.
23
34
  const tooltipId = useId();
24
35
 
@@ -40,13 +51,11 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
40
51
  ) {
41
52
  nextSide = "top";
42
53
  }
43
- const x = Math.min(
44
- Math.max(rect.left + rect.width / 2, VIEWPORT_MARGIN),
45
- window.innerWidth - VIEWPORT_MARGIN,
46
- );
47
54
  setResolvedSide(nextSide);
48
55
  setPos({
49
- x,
56
+ // Raw trigger centre; clamped to the viewport at render, once the
57
+ // bubble's own width is known (see clampedX).
58
+ x: rect.left + rect.width / 2,
50
59
  y: nextSide === "top" ? rect.top - 6 : rect.bottom + 6,
51
60
  });
52
61
  setVisible(true);
@@ -61,6 +70,12 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
61
70
  setVisible(false);
62
71
  }, []);
63
72
 
73
+ // Measure before paint so a wide bubble near a viewport edge is clamped in
74
+ // the same commit it appears in (no visible jump).
75
+ useLayoutEffect(() => {
76
+ setBubbleWidth(visible ? (bubbleRef.current?.offsetWidth ?? 0) : 0);
77
+ }, [visible, label]);
78
+
64
79
  // WCAG 1.4.13: tooltip content must be dismissible with Escape.
65
80
  useEffect(() => {
66
81
  if (!visible) return;
@@ -71,6 +86,8 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
71
86
  return () => document.removeEventListener("keydown", onKeyDown);
72
87
  }, [visible, hide]);
73
88
 
89
+ const clampedX = clampCentredLeft(pos.x, bubbleWidth, window.innerWidth, VIEWPORT_MARGIN);
90
+
74
91
  return (
75
92
  <>
76
93
  <span
@@ -89,12 +106,13 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
89
106
  <div
90
107
  className="fixed z-[200] pointer-events-none"
91
108
  style={{
92
- left: pos.x,
109
+ left: clampedX,
93
110
  top: pos.y,
94
111
  transform: resolvedSide === "top" ? "translate(-50%, -100%)" : "translate(-50%, 0)",
95
112
  }}
96
113
  >
97
114
  <div
115
+ ref={bubbleRef}
98
116
  role="tooltip"
99
117
  id={tooltipId}
100
118
  className="px-2 py-1 rounded-md bg-neutral-800 border border-neutral-700/50 text-[10px] font-medium text-neutral-200 whitespace-nowrap shadow-lg"
@@ -7,7 +7,6 @@ import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
7
7
  import { isEditableTarget } from "../utils/timelineDiscovery";
8
8
  import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
9
9
  import { canSplitElement } from "../utils/timelineElementSplit";
10
- import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
11
10
  import { trackStudioEvent } from "../utils/studioTelemetry";
12
11
  import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
13
12
 
@@ -258,7 +257,7 @@ function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks
258
257
  }
259
258
  }
260
259
 
261
- if (STUDIO_RAZOR_TOOL_ENABLED && key === "b" && !event.shiftKey && !event.altKey) {
260
+ if (key === "b" && !event.shiftKey && !event.altKey) {
262
261
  event.preventDefault();
263
262
  const { activeTool, setActiveTool } = usePlayerStore.getState();
264
263
  setActiveTool(activeTool === "razor" ? "select" : "razor");
@@ -4,7 +4,6 @@
4
4
  * Extracted from useDomEditSession to keep file sizes under the 600-line limit.
5
5
  */
6
6
  import { useEffect, useRef } from "react";
7
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
8
7
  import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
9
8
  import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
10
9
  import type { SidebarTab } from "../components/sidebar/LeftSidebar";
@@ -53,7 +52,7 @@ export function useDomEditPreviewSync({
53
52
 
54
53
  // fallow-ignore-next-line complexity
55
54
  const syncSelectionFromDocument = async () => {
56
- if (!STUDIO_INSPECTOR_PANELS_ENABLED || captionEditMode) return;
55
+ if (captionEditMode) return;
57
56
  const currentSelection = domEditSelectionRef.current;
58
57
  if (!currentSelection) return;
59
58
  let doc: Document | null = null;
@@ -9,7 +9,6 @@
9
9
  */
10
10
  import { useCallback, useEffect, useRef } from "react";
11
11
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
12
- import { STUDIO_GSAP_PANEL_ENABLED } from "../components/editor/manualEditingAvailability";
13
12
  import { usePlayerStore } from "../player";
14
13
  import { useDomEditPreviewSync } from "./useDomEditPreviewSync";
15
14
  import { useGsapAnimationsForElement, usePopulateKeyframeCacheForFile } from "./useGsapTweenCache";
@@ -195,7 +194,7 @@ export function useDomEditWiring({
195
194
  const gsapSourceFile = domEditSelection?.sourceFile || activeCompPath || "index.html";
196
195
 
197
196
  usePopulateKeyframeCacheForFile(
198
- STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null,
197
+ projectId ?? null,
199
198
  gsapSourceFile,
200
199
  gsapCacheVersion,
201
200
  previewIframeRef,
@@ -206,7 +205,7 @@ export function useDomEditWiring({
206
205
  multipleTimelines: gsapMultipleTimelines,
207
206
  unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
208
207
  } = useGsapAnimationsForElement(
209
- STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null,
208
+ projectId ?? null,
210
209
  gsapSourceFile,
211
210
  domEditSelection
212
211
  ? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null }
@@ -16,7 +16,6 @@ import {
16
16
  replaceDomEditGroupSelection,
17
17
  seedDomEditGroupWithSelection,
18
18
  } from "../utils/domEditHelpers";
19
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
20
19
  import {
21
20
  findElementForSelection,
22
21
  findElementForTimelineElement,
@@ -164,14 +163,6 @@ export function useDomSelection({
164
163
  setSelectedTimelineElementId(null);
165
164
  return;
166
165
  }
167
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
168
- domEditSelectionRef.current = null;
169
- domEditGroupSelectionsRef.current = [];
170
- setDomEditSelection(null);
171
- setDomEditGroupSelections([]);
172
- setSelectedTimelineElementId(null);
173
- return;
174
- }
175
166
 
176
167
  const isAdditiveSelection = Boolean(options?.additive);
177
168
  const currentSelection = domEditSelectionRef.current;
@@ -370,7 +361,6 @@ export function useDomSelection({
370
361
 
371
362
  const handleTimelineElementSelect = useCallback(
372
363
  async (element: TimelineElement | null) => {
373
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
374
364
  const seq = ++timelineSelectSeqRef.current;
375
365
  if (!element) {
376
366
  applyDomSelection(null, { revealPanel: false });
@@ -513,14 +503,6 @@ export function useDomSelection({
513
503
  const applyMarqueeSelection = useCallback(
514
504
  // fallow-ignore-next-line complexity
515
505
  (selections: DomEditSelection[], additive: boolean) => {
516
- // Honor the inspector-panels kill switch like applyDomSelection does.
517
- if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
518
- domEditSelectionRef.current = null;
519
- domEditGroupSelectionsRef.current = [];
520
- setDomEditSelection(null);
521
- setDomEditGroupSelections([]);
522
- return;
523
- }
524
506
  if (selections.length === 0) {
525
507
  if (!additive) applyDomSelection(null, { revealPanel: false });
526
508
  return;
@@ -557,15 +539,6 @@ export function useDomSelection({
557
539
  [applyDomSelection, timelineElements, setSelectedTimelineElementId],
558
540
  );
559
541
 
560
- // Disabled inspector effect
561
- // eslint-disable-next-line no-restricted-syntax
562
- useEffect(() => {
563
- if (STUDIO_INSPECTOR_PANELS_ENABLED) return;
564
- updateDomEditHoverSelection(null);
565
- applyDomSelection(null, { revealPanel: false });
566
- if (rightPanelTab !== "renders") setRightPanelTab("renders");
567
- }, [applyDomSelection, rightPanelTab, updateDomEditHoverSelection, setRightPanelTab]);
568
-
569
542
  return {
570
543
  // State
571
544
  domEditSelection,
@@ -1,7 +1,6 @@
1
1
  import { useCallback, useRef } from "react";
2
2
  import { liveTime, usePlayerStore } from "../player";
3
3
  import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers";
4
- import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability";
5
4
  import { type DomEditSelection } from "../components/editor/domEditing";
6
5
  import type { ApplyDomSelectionOptions, ResolveDomSelectionOptions } from "./useDomSelection";
7
6
  import { trackStudioEvent } from "../utils/studioTelemetry";
@@ -87,7 +86,7 @@ export function usePreviewInteraction({
87
86
  const handlePreviewCanvasMouseDown = useCallback(
88
87
  // fallow-ignore-next-line complexity
89
88
  async (e: React.MouseEvent<HTMLDivElement>, options?: PreviewMouseDownOptions) => {
90
- if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return;
89
+ if (captionEditMode || compositionLoading) return;
91
90
 
92
91
  // Manual double-click detection (see DOUBLE_CLICK_MS): the first click
93
92
  // re-renders the overlay so `e.detail` never reaches 2 on the canvas.
@@ -236,7 +235,7 @@ export function usePreviewInteraction({
236
235
  const handlePreviewCanvasPointerMove = useCallback(
237
236
  // fallow-ignore-next-line complexity
238
237
  async (e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
239
- if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) {
238
+ if (captionEditMode || compositionLoading) {
240
239
  updateDomEditHoverSelection(null);
241
240
  return null;
242
241
  }
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useMemo, useRef, useState, type DragEvent } from "react";
2
- import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
3
2
  import type { DomEditSelection } from "../components/editor/domEditing";
4
3
  import type { StudioContextValue } from "../contexts/StudioContext";
5
4
  import type { RightInspectorPanes } from "../utils/studioHelpers";
@@ -85,17 +84,14 @@ export function useInspectorState(
85
84
  // fallow-ignore-next-line complexity
86
85
  return useMemo(() => {
87
86
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
88
- const layersPanelActive =
89
- STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.layers;
90
- const designPanelActive =
91
- STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.design;
87
+ const layersPanelActive = inspectorTabActive && rightInspectorPanes.layers;
88
+ const designPanelActive = inspectorTabActive && rightInspectorPanes.design;
92
89
  const inspectorPanelActive = layersPanelActive || designPanelActive;
93
90
  return {
94
91
  layersPanelActive,
95
92
  designPanelActive,
96
93
  inspectorPanelActive,
97
- inspectorButtonActive:
98
- STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive,
94
+ inspectorButtonActive: !rightCollapsed && inspectorPanelActive,
99
95
  // Deliberately wider than shouldShowSelectedDomBounds: the on-canvas path
100
96
  // handles ARE the arc-drag affordance, so gating them on an open Inspector
101
97
  // would make keyframe path editing reachable only from a side panel.
@@ -32,7 +32,6 @@ import {
32
32
  useTimelineTrackLayout,
33
33
  } from "./useTimelineTrackLayout";
34
34
  import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
35
- import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
36
35
  import { useTrackGapMenu } from "./useTrackGapMenu";
37
36
  import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
38
37
  import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext";
@@ -126,7 +125,7 @@ export const Timeline = memo(function Timeline({
126
125
  ),
127
126
  [gsapAnimations],
128
127
  );
129
- const labelMode = STUDIO_KEYFRAMES_ENABLED && hasKeyframedClips;
128
+ const labelMode = hasKeyframedClips;
130
129
  // Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD
131
130
  // (dropping it would jam clip 0 against the gutter on every non-keyframed
132
131
  // composition); in label mode the 232px label column already provides it.
@@ -17,7 +17,6 @@ import {
17
17
  } from "./timelineMultiDragPreview";
18
18
  import type { TimelineLaneBaseProps } from "./timelineLaneProps";
19
19
  import type { TimelineEditCallbacks } from "./timelineCallbacks";
20
- import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
21
20
  import { trackStudioKeyframeLaneExpand } from "../../telemetry/events";
22
21
  import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
23
22
  import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
@@ -134,9 +133,12 @@ export function TimelineLanes({
134
133
  // The one keyframed element this track shows lanes for (selected, else
135
134
  // most lanes). A track can hold several elements; scoping to one keeps
136
135
  // their keyframes from cramming into a single row.
137
- const keyframeClip = STUDIO_KEYFRAMES_ENABLED
138
- ? resolveTrackKeyframeClip(els, laneCounts, selectedElementId, selectedElementIds)
139
- : null;
136
+ const keyframeClip = resolveTrackKeyframeClip(
137
+ els,
138
+ laneCounts,
139
+ selectedElementId,
140
+ selectedElementIds,
141
+ );
140
142
  const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
141
143
  const keyframeClipExpanded =
142
144
  keyframeClipKey != null && expandedClipIds.has(keyframeClipKey);
@@ -249,8 +251,7 @@ export function TimelineLanes({
249
251
  // Only the track's active keyframe clip shows expanded lanes;
250
252
  // other clips (incl. siblings on a shared track) show compact
251
253
  // diamonds on their own bar instead.
252
- const isTrackKeyframeClip =
253
- STUDIO_KEYFRAMES_ENABLED && elementKey === keyframeClipKey;
254
+ const isTrackKeyframeClip = elementKey === keyframeClipKey;
254
255
  const showsLanes = isTrackKeyframeClip && keyframeClipExpanded;
255
256
  const capabilities = getTimelineEditCapabilities(el);
256
257
  const isSelected =
@@ -428,35 +429,32 @@ export function TimelineLanes({
428
429
  renderClipContent,
429
430
  renderClipOverlay,
430
431
  )}
431
- {STUDIO_KEYFRAMES_ENABLED &&
432
- !showsLanes &&
433
- keyframeCache?.get(elementKey) && (
434
- <TimelineClipDiamonds
435
- keyframesData={keyframeCache.get(elementKey)!}
436
- clipWidthPx={Math.max(previewElement.duration * pps, 4)}
437
- clipHeightPx={rowHeight - 2 * CLIP_Y}
438
- beatsActive={beatStripOnTrack}
439
- accentColor={clipStyle.accent}
440
- isSelected={isSelected}
441
- currentPercentage={
442
- previewElement.duration > 0
443
- ? ((currentTime - previewElement.start) /
444
- previewElement.duration) *
445
- 100
446
- : 0
447
- }
448
- elementId={elementKey}
449
- selectedKeyframes={selectedKeyframes}
450
- onClickKeyframe={(_elId, target) =>
451
- onClickKeyframe?.(previewElement, target)
452
- }
453
- onShiftClickKeyframe={onShiftClickKeyframe}
454
- onContextMenuKeyframe={onContextMenuKeyframe}
455
- onMoveKeyframe={onMoveKeyframe}
456
- onSelectSegment={onSelectSegment}
457
- suppressClickRef={suppressClickRef}
458
- />
459
- )}
432
+ {!showsLanes && keyframeCache?.get(elementKey) && (
433
+ <TimelineClipDiamonds
434
+ keyframesData={keyframeCache.get(elementKey)!}
435
+ clipWidthPx={Math.max(previewElement.duration * pps, 4)}
436
+ clipHeightPx={rowHeight - 2 * CLIP_Y}
437
+ beatsActive={beatStripOnTrack}
438
+ accentColor={clipStyle.accent}
439
+ isSelected={isSelected}
440
+ currentPercentage={
441
+ previewElement.duration > 0
442
+ ? ((currentTime - previewElement.start) / previewElement.duration) *
443
+ 100
444
+ : 0
445
+ }
446
+ elementId={elementKey}
447
+ selectedKeyframes={selectedKeyframes}
448
+ onClickKeyframe={(_elId, target) =>
449
+ onClickKeyframe?.(previewElement, target)
450
+ }
451
+ onShiftClickKeyframe={onShiftClickKeyframe}
452
+ onContextMenuKeyframe={onContextMenuKeyframe}
453
+ onMoveKeyframe={onMoveKeyframe}
454
+ onSelectSegment={onSelectSegment}
455
+ suppressClickRef={suppressClickRef}
456
+ />
457
+ )}
460
458
  </TimelineClip>
461
459
  );
462
460
  // Mounted for the track's keyframe clip in BOTH disclosure
@@ -1,7 +1,6 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
3
3
  import { usePlayerStore } from "../store/playerStore";
4
- import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
5
4
  import { useStudioShellContextOptional } from "../../contexts/StudioContext";
6
5
  import { animationContributesLane } from "./TimelinePropertyLanes";
7
6
 
@@ -34,7 +33,6 @@ export function useAutoExpandKeyframedClips(gsapAnimations: Map<string, GsapAnim
34
33
  const projectId = useStudioShellContextOptional()?.projectId ?? null;
35
34
  const seen = useRef({ projectId, source: gsapAnimations, clips: new Set<string>() });
36
35
  useEffect(() => {
37
- if (!STUDIO_KEYFRAMES_ENABLED) return;
38
36
  if (seen.current.projectId !== projectId) {
39
37
  const sourceChanged = seen.current.source !== gsapAnimations;
40
38
  seen.current = { projectId, source: gsapAnimations, clips: new Set() };