chore(studio): remove fully rolled-out studio feature flags (#2889)

## What

Removes six Studio feature flags that have been default-`true` for 7+ weeks. Each is reachable under two env names, so this deletes **12 `VITE_STUDIO_*` env vars**:

| Flag constant | Env names removed | Default-on since |
|---|---|---|
| `STUDIO_PREVIEW_MANUAL_EDITING_ENABLED` | `VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING`, `VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED` | 2026-05-12 |
| `STUDIO_INSPECTOR_PANELS_ENABLED` (+ its `STUDIO_PREVIEW_SELECTION_ENABLED` alias) | `VITE_STUDIO_ENABLE_INSPECTOR_PANELS`, `VITE_STUDIO_INSPECTOR_PANELS_ENABLED` | 2026-05-12 |
| `STUDIO_BLOCKS_PANEL_ENABLED` | `VITE_STUDIO_ENABLE_BLOCKS_PANEL`, `VITE_STUDIO_BLOCKS_PANEL_ENABLED` | 2026-05-18 |
| `STUDIO_GSAP_PANEL_ENABLED` | `VITE_STUDIO_ENABLE_GSAP_PANEL`, `VITE_STUDIO_GSAP_PANEL_ENABLED` | 2026-05-28 |
| `STUDIO_KEYFRAMES_ENABLED` | `VITE_STUDIO_ENABLE_KEYFRAMES`, `VITE_STUDIO_KEYFRAMES_ENABLED` | 2026-06-05 |
| `STUDIO_RAZOR_TOOL_ENABLED` | `VITE_STUDIO_ENABLE_RAZOR_TOOL`, `VITE_STUDIO_RAZOR_TOOL_ENABLED` | 2026-06-10 |

## Why

Every one of these shipped as a rollout gate, went to `true`, and then stayed. Because none of them was ever flipped back, the `false` branch was unreachable in practice while still costing a real import, a real conditional, and a real "what happens if this is off?" question at ~90 call sites across 25 files.

The bigger cost is what the dead branch kept alive. Removing the flags also removes the disabled-Studio code paths that only existed to serve them:

- the greyed-out, `disabled`, "Manual editing is temporarily disabled" Inspector button in `StudioHeader` (and the `STUDIO_MANUAL_EDITING_DISABLED_TITLE` constant behind it)
- the inspector-off reset `useEffect` in `useDomSelection`, which force-cleared selection and redirected the right panel to Renders
- three selection kill-switch early-returns in `useDomSelection` (`applyDomSelection`, `handleTimelineElementSelect`, `applyMarqueeSelection`)
- the tab-redirect branch in `normalizeStudioUrlPanelTab`, whose `options.inspectorPanelsEnabled` parameter had no production caller at all (only tests passed it)

## How

No behavior change: every flag was removed by keeping its default-`true` side.

The call-site edits are three mechanical boolean shapes (`X && rest` → `rest`, `rest && X` → `rest`, `!X || rest` → `rest`), applied by script for uniformity. Everything else (ternaries, `if` guards, unreachable blocks, JSX wrappers that had no other condition) was done by hand and the whole diff was read line by line afterwards.

`resolveStudioBooleanEnvFlag` and the `import.meta.env` / `window.__HF_STUDIO_ENV__` plumbing stay: three flags still use them (`STUDIO_FLAT_INSPECTOR_ENABLED`, `STUDIO_SDK_CUTOVER_ENABLED`, `STUDIO_SDK_RESOLVER_SHADOW_ENABLED`). Its unit tests kept their coverage but now exercise a live flag pair instead of retired env names, so no dead `VITE_STUDIO_*` string is left in the repo.

Net **-191 lines** (236 insertions, 427 deletions across 25 files); most insertions are reindentation of JSX that lost a wrapper.

### Deliberately not in scope

Flags authored by other people are untouched, even where they look similarly settled:

- `VITE_STUDIO_ENABLE_FLAT_INSPECTOR` / `VITE_STUDIO_FLAT_INSPECTOR_ENABLED` (default true, but not mine)
- `VITE_STUDIO_SDK_CUTOVER_ENABLED`, `VITE_STUDIO_SDK_CUTOVER_FAMILIES`, `VITE_STUDIO_SDK_RESOLVER_SHADOW_ENABLED` (SDK cutover canary, still soaking)
- `VITE_HYPERFRAMES_NO_TELEMETRY`

Mine but genuinely long-lived configuration rather than rollout gates, so they stay: `VITE_STUDIO_DISCOVERY_PORTS`, `VITE_HYPERFRAMES_FEEDBACK_INTERVAL`, `VITE_HYPERFRAMES_NO_FEEDBACK` (a documented user opt-out), plus the `HYPERFRAMES_*` binary paths, API URLs, cache sizes, and timeouts.

`VITE_STUDIO_ENABLE_MOTION_PANEL` / `VITE_STUDIO_MOTION_PANEL_ENABLED` were already retired from production code before this PR; they only survived as placeholder names inside the resolver's unit tests, and this PR swaps those out.

## Test plan

- [x] Unit tests added/updated - dropped the two tests asserting removed flag defaults; retargeted the `resolveStudioBooleanEnvFlag` cases at a live flag pair; updated `studioUrlState` tests for the narrowed `normalizeStudioUrlPanelTab` signature (now also asserts an unknown tab returns `null`).
- [x] Manual testing performed - see below.
- [ ] Documentation updated (if applicable) - not needed; no removed name appears in `docs/`, `skills/`, or `registry/`. (`docs/changelog.mdx` has one historical entry naming `STUDIO_KEYFRAMES_ENABLED`; changelog history is left as written.)

```
packages/studio: bunx vitest run          # 280 files, 3116 tests pass, 1 skipped
packages/studio: bunx tsc --noEmit        # clean
bun run build                             # green (all packages)
bunx oxlint  <25 changed files>           # 0 warnings, 0 errors
bunx oxfmt --check <25 changed files>     # clean
```

Two extra checks, because part of this diff was script-generated:

1. Zero references to any removed flag constant or env name remain anywhere outside `docs/changelog.mdx`.
2. Diffed every string literal in each changed non-test file against `origin/main`. The only differences are the intended removals: the 12 env names, `"Manual editing is temporarily disabled"`, the `"cursor-not-allowed …"` disabled class, the 3-column `"1fr 1fr 1fr"` grid, and the `"renders"` redirect literals. No user-facing label, tooltip, or class string changed by accident.
This commit is contained in:
Miguel Ángel
2026-07-30 02:00:05 +02:00
committed by GitHub
parent 860954d71c
commit cef3b86c95
25 changed files with 236 additions and 427 deletions
+2 -5
View File
@@ -43,7 +43,6 @@ import {
import type { DomEditSelection } from "./components/editor/domEditing"; import type { DomEditSelection } from "./components/editor/domEditing";
import { StudioHeader } from "./components/StudioHeader"; import { StudioHeader } from "./components/StudioHeader";
import { useGestureCommit } from "./hooks/useGestureCommit"; import { useGestureCommit } from "./hooks/useGestureCommit";
import { STUDIO_KEYFRAMES_ENABLED } from "./components/editor/manualEditingAvailability";
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay"; import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
import { StudioLeftSidebar } from "./components/StudioLeftSidebar"; import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
import { EditorShell } from "./components/EditorShell"; import { EditorShell } from "./components/EditorShell";
@@ -263,9 +262,7 @@ export function StudioApp() {
onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(), onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
activeCompPath, activeCompPath,
forceReloadSdkSession: sdkHandle.forceReload, forceReloadSdkSession: sdkHandle.forceReload,
onToggleRecording: STUDIO_KEYFRAMES_ENABLED onToggleRecording: () => handleToggleRecordingRef.current(),
? () => handleToggleRecordingRef.current()
: undefined,
}); });
const sidebarTabRef = useRef({ const sidebarTabRef = useRef({
select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t), select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t),
@@ -367,7 +364,7 @@ export function StudioApp() {
isGestureRecordingRef, isGestureRecordingRef,
}); });
handleToggleRecordingRef.current = handleToggleRecording; handleToggleRecordingRef.current = handleToggleRecording;
const recordingToggle = STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : undefined; const recordingToggle = handleToggleRecording;
const canvasRectRef = useRef<DOMRect | null>(null); const canvasRectRef = useRef<DOMRect | null>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
if (gestureState !== "recording" || !previewIframe) { if (gestureState !== "recording" || !previewIframe) {
@@ -1,9 +1,5 @@
import { useRef, type MouseEvent } from "react"; import { useRef, type MouseEvent } from "react";
import { RotateCcw, RotateCw, Camera } from "../icons/SystemIcons"; import { RotateCcw, RotateCw, Camera } from "../icons/SystemIcons";
import {
STUDIO_INSPECTOR_PANELS_ENABLED,
STUDIO_MANUAL_EDITING_DISABLED_TITLE,
} from "./editor/manualEditingAvailability";
import { getHistoryShortcutLabel } from "../utils/studioHelpers"; import { getHistoryShortcutLabel } from "../utils/studioHelpers";
import { useStudioShellContext } from "../contexts/StudioContext"; import { useStudioShellContext } from "../contexts/StudioContext";
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext"; import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
@@ -328,16 +324,10 @@ export function StudioHeader({
<span>{capturing ? "Capturing…" : "Capture"}</span> <span>{capturing ? "Capturing…" : "Capture"}</span>
</a> </a>
</Tooltip> </Tooltip>
<Tooltip <Tooltip label="Inspector" side="bottom">
label={
STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
}
side="bottom"
>
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
if (rightCollapsed || !inspectorPanelActive) { if (rightCollapsed || !inspectorPanelActive) {
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false }); trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
setRightPanelTab("design"); setRightPanelTab("design");
@@ -349,18 +339,13 @@ export function StudioHeader({
// the panel shouldn't deselect the element. // the panel shouldn't deselect the element.
setRightCollapsed(true); setRightCollapsed(true);
}} }}
disabled={!STUDIO_INSPECTOR_PANELS_ENABLED}
aria-pressed={inspectorButtonActive} aria-pressed={inspectorButtonActive}
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors active:scale-[0.98] ${ className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors active:scale-[0.98] ${
inspectorButtonActive inspectorButtonActive
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30" ? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
: STUDIO_INSPECTOR_PANELS_ENABLED : "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
? "text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800 border-transparent"
: "cursor-not-allowed border-transparent text-neutral-700"
}`} }`}
aria-label={ aria-label="Inspector"
STUDIO_INSPECTOR_PANELS_ENABLED ? "Inspector" : STUDIO_MANUAL_EDITING_DISABLED_TITLE
}
> >
<svg <svg
width="12" width="12"
@@ -10,10 +10,7 @@ import { PanelTabButton } from "./PanelTabButton";
import { usePreviewVariablesStore } from "../hooks/previewVariablesStore"; import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
import type { RenderJob } from "./renders/useRenderQueue"; import type { RenderJob } from "./renders/useRenderQueue";
import type { BlockParam } from "@hyperframes/core/registry"; import type { BlockParam } from "@hyperframes/core/registry";
import { import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
STUDIO_FLAT_INSPECTOR_ENABLED,
STUDIO_INSPECTOR_PANELS_ENABLED,
} from "./editor/manualEditingAvailability";
import type { Composition } from "@hyperframes/sdk"; import type { Composition } from "@hyperframes/sdk";
import type { EditHistoryKind } from "../utils/editHistory"; import type { EditHistoryKind } from "../utils/editHistory";
import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist"; import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
@@ -229,8 +226,7 @@ export function StudioRightPanel({
setRightPanelTab, setRightPanelTab,
}); });
const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive; const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
const layersPaneOpen = const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers;
inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;
const handleInspectorPaneButtonClick = (pane: "design" | "layers") => { const handleInspectorPaneButtonClick = (pane: "design" | "layers") => {
if (!inspectorTabActive) { if (!inspectorTabActive) {
@@ -483,22 +479,18 @@ export function StudioRightPanel({
) : ( ) : (
<> <>
<div className="flex min-w-0 items-center gap-1 overflow-hidden border-b border-neutral-800 px-3 py-2"> <div className="flex min-w-0 items-center gap-1 overflow-hidden border-b border-neutral-800 px-3 py-2">
{STUDIO_INSPECTOR_PANELS_ENABLED && ( <PanelTabButton
<> label="Design"
<PanelTabButton tooltip="Element styles and properties"
label="Design" active={designPaneOpen}
tooltip="Element styles and properties" onClick={() => handleInspectorPaneButtonClick("design")}
active={designPaneOpen} />
onClick={() => handleInspectorPaneButtonClick("design")} <PanelTabButton
/> label="Layers"
<PanelTabButton tooltip="Composition layer stack"
label="Layers" active={layersPaneOpen}
tooltip="Composition layer stack" onClick={() => handleInspectorPaneButtonClick("layers")}
active={layersPaneOpen} />
onClick={() => handleInspectorPaneButtonClick("layers")}
/>
</>
)}
<PanelTabButton <PanelTabButton
label={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"} label={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
tooltip="Render queue and exports" tooltip="Render queue and exports"
+120 -132
View File
@@ -15,10 +15,6 @@ import {
} from "../player/components/timelineZoom"; } from "../player/components/timelineZoom";
import { useTimelineZoom } from "../player/components/useTimelineZoom"; import { useTimelineZoom } from "../player/components/useTimelineZoom";
import { usePlayerStore, type TimelineElement } from "../player"; import { usePlayerStore, type TimelineElement } from "../player";
import {
STUDIO_KEYFRAMES_ENABLED,
STUDIO_RAZOR_TOOL_ENABLED,
} from "./editor/manualEditingAvailability";
import { Tooltip } from "./ui"; import { Tooltip } from "./ui";
import { Scissors } from "../icons/SystemIcons"; import { Scissors } from "../icons/SystemIcons";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
@@ -135,7 +131,7 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
// Wire the "Add keyframe (K)" shortcut the toolbar advertises. Active only when // Wire the "Add keyframe (K)" shortcut the toolbar advertises. Active only when
// there's a keyframeable selection; otherwise K stays JKL-pause in playback. // there's a keyframeable selection; otherwise K stays JKL-pause in playback.
useKeyframeKeyboard({ useKeyframeKeyboard({
enabled: STUDIO_KEYFRAMES_ENABLED && Boolean(onToggleKeyframe), enabled: Boolean(onToggleKeyframe),
onAddKeyframe: onToggleKeyframe, onAddKeyframe: onToggleKeyframe,
}); });
@@ -170,36 +166,32 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
<div className="border-b border-neutral-800/60"> <div className="border-b border-neutral-800/60">
<div className="flex items-center justify-between px-2 py-0.5"> <div className="flex items-center justify-between px-2 py-0.5">
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
{STUDIO_RAZOR_TOOL_ENABLED && ( <Tooltip label="Selection tool (V)">
<> <button
<Tooltip label="Selection tool (V)"> type="button"
<button onClick={() => setActiveTool("select")}
type="button" aria-label="Selection tool"
onClick={() => setActiveTool("select")} aria-pressed={activeTool === "select"}
aria-label="Selection tool" className={activeTool === "select" ? flatActive : flatIdle}
aria-pressed={activeTool === "select"} >
className={activeTool === "select" ? flatActive : flatIdle} <svg width="16" height="16" viewBox="0 0 12 12" fill="currentColor">
> <path d="M2 0.5L10 6L6.5 6.5L8.5 11L6.5 11.5L4.5 7L2 9Z" />
<svg width="16" height="16" viewBox="0 0 12 12" fill="currentColor"> </svg>
<path d="M2 0.5L10 6L6.5 6.5L8.5 11L6.5 11.5L4.5 7L2 9Z" /> </button>
</svg> </Tooltip>
</button> <Tooltip label="Razor tool (B) — Shift+click splits all tracks">
</Tooltip> <button
<Tooltip label="Razor tool (B) — Shift+click splits all tracks"> type="button"
<button onClick={() => setActiveTool("razor")}
type="button" aria-label="Razor tool"
onClick={() => setActiveTool("razor")} aria-pressed={activeTool === "razor"}
aria-label="Razor tool" className={activeTool === "razor" ? flatActive : flatIdle}
aria-pressed={activeTool === "razor"} >
className={activeTool === "razor" ? flatActive : flatIdle} <Scissors size={16} />
> </button>
<Scissors size={16} /> </Tooltip>
</button> {/* Divider: tool-mode | editing-actions */}
</Tooltip> <div aria-hidden="true" className="mx-1 h-4 w-px bg-neutral-800" />
{/* Divider: tool-mode | editing-actions */}
<div aria-hidden="true" className="mx-1 h-4 w-px bg-neutral-800" />
</>
)}
<Tooltip label={timelineSnapEnabled ? "Snapping on (N)" : "Snapping off (N)"}> <Tooltip label={timelineSnapEnabled ? "Snapping on (N)" : "Snapping off (N)"}>
<button <button
type="button" type="button"
@@ -211,111 +203,107 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
<Magnet size={16} weight="bold" aria-hidden="true" /> <Magnet size={16} weight="bold" aria-hidden="true" />
</button> </button>
</Tooltip> </Tooltip>
{STUDIO_KEYFRAMES_ENABLED && ( {/* Always rendered (CapCut-style): with no keyframeable selection the
// Always rendered (CapCut-style): with no keyframeable selection the button fades to a disabled state instead of unmounting, so the
// button fades to a disabled state instead of unmounting, so the toolbar layout never shifts. */}
// toolbar layout never shifts. <Tooltip
<Tooltip label={
label={ keyframePathEndpoint
? "Motion path endpoints cannot be removed"
: !onToggleKeyframe
? "Select an animated element to add keyframes"
: keyframeIsMotionPath
? keyframeWillExtend
? "Extend motion path to playhead (K)"
: keyframeState === "active"
? "Remove waypoint from motion path (K)"
: "Add waypoint to motion path (K)"
: keyframeState === "active"
? "Remove keyframe at playhead (K)"
: keyframeState === "inactive"
? keyframeWillExtend
? "Add keyframe at playhead, extends animation (K)"
: "Add keyframe at playhead (K)"
: "Add keyframe (K)"
}
>
<button
type="button"
disabled={!onToggleKeyframe}
onClick={onToggleKeyframe}
aria-label={
keyframePathEndpoint keyframePathEndpoint
? "Motion path endpoints cannot be removed" ? "Motion path endpoint"
: !onToggleKeyframe : keyframeIsMotionPath
? "Select an animated element to add keyframes" ? keyframeState === "active"
: keyframeIsMotionPath ? "Remove motion path waypoint"
? keyframeWillExtend : keyframeWillExtend
? "Extend motion path to playhead (K)" ? "Extend motion path to playhead"
: keyframeState === "active" : "Add motion path waypoint"
? "Remove waypoint from motion path (K)" : keyframeState === "active"
: "Add waypoint to motion path (K)" ? "Remove keyframe at playhead"
: keyframeState === "active" : "Add keyframe at playhead"
? "Remove keyframe at playhead (K)" }
className={
!onToggleKeyframe
? flatDisabled
: `${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
keyframeState === "active"
? "text-studio-accent"
: keyframeState === "inactive" : keyframeState === "inactive"
? keyframeWillExtend ? "text-neutral-400 hover:text-studio-accent"
? "Add keyframe at playhead, extends animation (K)" : "text-neutral-600 hover:text-neutral-400"
: "Add keyframe at playhead (K)" }`
: "Add keyframe (K)"
} }
> >
<button <svg width="16" height="16" viewBox="0 0 10 10" fill="currentColor">
type="button" {keyframeState === "active" ? (
disabled={!onToggleKeyframe} <path d="M5 0.5L9.5 5L5 9.5L0.5 5Z" />
onClick={onToggleKeyframe} ) : (
aria-label={ <path
keyframePathEndpoint d="M5 1.2L8.8 5L5 8.8L1.2 5Z"
? "Motion path endpoint" fill="none"
: keyframeIsMotionPath stroke="currentColor"
? keyframeState === "active" strokeWidth="1.2"
? "Remove motion path waypoint" />
: keyframeWillExtend )}
? "Extend motion path to playhead" </svg>
: "Add motion path waypoint" </button>
: keyframeState === "active" </Tooltip>
? "Remove keyframe at playhead" <Tooltip
: "Add keyframe at playhead" label={
} autoKeyframeEnabled
className={ ? "Auto-record manual edits as keyframes (click to turn off)"
!onToggleKeyframe : "Manual edits will not be recorded as keyframes (click to turn on)"
? flatDisabled }
: `${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${ >
keyframeState === "active" <button
? "text-studio-accent" type="button"
: keyframeState === "inactive" onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
? "text-neutral-400 hover:text-studio-accent" aria-label="Auto-record manual edits as keyframes"
: "text-neutral-600 hover:text-neutral-400" aria-pressed={autoKeyframeEnabled}
}` className={`${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
}
>
<svg width="16" height="16" viewBox="0 0 10 10" fill="currentColor">
{keyframeState === "active" ? (
<path d="M5 0.5L9.5 5L5 9.5L0.5 5Z" />
) : (
<path
d="M5 1.2L8.8 5L5 8.8L1.2 5Z"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
/>
)}
</svg>
</button>
</Tooltip>
)}
{STUDIO_KEYFRAMES_ENABLED && (
<Tooltip
label={
autoKeyframeEnabled autoKeyframeEnabled
? "Auto-record manual edits as keyframes (click to turn off)" ? "text-red-400 hover:text-red-300"
: "Manual edits will not be recorded as keyframes (click to turn on)" : "text-neutral-600 hover:text-neutral-400"
} }`}
> >
<button <svg width="16" height="16" viewBox="0 0 10 10" fill="none">
type="button" {/* Same diamond outline as the Add-keyframe icon, with a
onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
aria-label="Auto-record manual edits as keyframes"
aria-pressed={autoKeyframeEnabled}
className={`${flatBtn} active:scale-[0.98] hover:bg-white/[0.06] ${
autoKeyframeEnabled
? "text-red-400 hover:text-red-300"
: "text-neutral-600 hover:text-neutral-400"
}`}
>
<svg width="16" height="16" viewBox="0 0 10 10" fill="none">
{/* Same diamond outline as the Add-keyframe icon, with a
record-style dot inside: filled = auto-recording, record-style dot inside: filled = auto-recording,
hollow = manual edits won't be keyframed. */} hollow = manual edits won't be keyframed. */}
<path d="M5 0.7L9.3 5L5 9.3L0.7 5Z" stroke="currentColor" strokeWidth="1" /> <path d="M5 0.7L9.3 5L5 9.3L0.7 5Z" stroke="currentColor" strokeWidth="1" />
<circle <circle
cx="5" cx="5"
cy="5" cy="5"
r="1.8" r="1.8"
fill={autoKeyframeEnabled ? "currentColor" : "none"} fill={autoKeyframeEnabled ? "currentColor" : "none"}
stroke="currentColor" stroke="currentColor"
strokeWidth="1" strokeWidth="1"
/> />
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
)}
{onSplitElement && {onSplitElement &&
(() => { (() => {
// Render the button unconditionally (disabled when unusable): // Render the button unconditionally (disabled when unusable):
@@ -447,9 +447,8 @@ describe("PropertyPanel — Motion group (Plan 3b)", () => {
it( it(
"hides the effect list (showEffects off) when the GSAP edit handlers are absent", "hides the effect list (showEffects off) when the GSAP edit handlers are absent",
async () => { async () => {
// STUDIO_GSAP_PANEL_ENABLED defaults on, but none of the five required // None of the five required edit handlers are supplied here, so the
// edit handlers are supplied here, so the effect-list half of the // effect list stays closed — only the Timing row shows.
// double-gate stays closed — only the Timing row shows.
const { host, root } = await renderPanel(true, animatedElement()); const { host, root } = await renderPanel(true, animatedElement());
openFlatGroup(host, "Motion"); openFlatGroup(host, "Motion");
const openGroup = openGroupText(host); const openGroup = openGroupText(host);
@@ -26,11 +26,7 @@ import { TextSection, StyleSections } from "./propertyPanelSections";
import { GsapAnimationSection } from "./GsapAnimationSection"; import { GsapAnimationSection } from "./GsapAnimationSection";
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform"; import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
import { KeyframeNavigation } from "./KeyframeNavigation"; import { KeyframeNavigation } from "./KeyframeNavigation";
import { import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./manualEditingAvailability";
STUDIO_FLAT_INSPECTOR_ENABLED,
STUDIO_GSAP_PANEL_ENABLED,
STUDIO_KEYFRAMES_ENABLED,
} from "./manualEditingAvailability";
import { PropertyPanelFlat } from "./PropertyPanelFlat"; import { PropertyPanelFlat } from "./PropertyPanelFlat";
import { createGsapLivePreview } from "./gsapLivePreview"; import { createGsapLivePreview } from "./gsapLivePreview";
import { usePlayerStore, liveTime } from "../../player"; import { usePlayerStore, liveTime } from "../../player";
@@ -396,7 +392,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
onCommit={(next) => commitManualOffset("x", next)} onCommit={(next) => commitManualOffset("x", next)}
/> />
</div> </div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && ( {gsapAnimId && (
<KeyframeNavigation <KeyframeNavigation
property="x" property="x"
keyframes={navKeyframes} keyframes={navKeyframes}
@@ -423,7 +419,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
onCommit={(next) => commitManualOffset("y", next)} onCommit={(next) => commitManualOffset("y", next)}
/> />
</div> </div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && ( {gsapAnimId && (
<KeyframeNavigation <KeyframeNavigation
property="y" property="y"
keyframes={navKeyframes} keyframes={navKeyframes}
@@ -450,7 +446,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
onCommit={(next) => commitManualSize("width", next)} onCommit={(next) => commitManualSize("width", next)}
/> />
</div> </div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && ( {gsapAnimId && (
<KeyframeNavigation <KeyframeNavigation
property="width" property="width"
keyframes={navKeyframes} keyframes={navKeyframes}
@@ -477,7 +473,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
onCommit={(next) => commitManualSize("height", next)} onCommit={(next) => commitManualSize("height", next)}
/> />
</div> </div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && ( {gsapAnimId && (
<KeyframeNavigation <KeyframeNavigation
property="height" property="height"
keyframes={navKeyframes} keyframes={navKeyframes}
@@ -503,7 +499,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
onCommit={(next) => commitManualRotation(next.replace("°", ""))} onCommit={(next) => commitManualRotation(next.replace("°", ""))}
/> />
</div> </div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && ( {gsapAnimId && (
<KeyframeNavigation <KeyframeNavigation
property="rotation" property="rotation"
keyframes={navKeyframes} keyframes={navKeyframes}
@@ -551,8 +547,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
</Section> </Section>
)} )}
{STUDIO_GSAP_PANEL_ENABLED && {onUpdateGsapProperty &&
onUpdateGsapProperty &&
onUpdateGsapMeta && onUpdateGsapMeta &&
onDeleteGsapAnimation && onDeleteGsapAnimation &&
onAddGsapProperty && onAddGsapProperty &&
@@ -16,7 +16,6 @@ import { FlatMediaSection } from "./propertyPanelFlatMediaSection";
import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation"; import { deriveElementTiming } from "./propertyPanelFlatTimingDerivation";
import { createGsapLivePreview } from "./gsapLivePreview"; import { createGsapLivePreview } from "./gsapLivePreview";
import { formatTextFieldPreview } from "./propertyPanelSections"; import { formatTextFieldPreview } from "./propertyPanelSections";
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
import { useColorGradingController } from "./useColorGradingController"; import { useColorGradingController } from "./useColorGradingController";
import { usePlayerStore } from "../../player"; import { usePlayerStore } from "../../player";
import { import {
@@ -224,7 +223,6 @@ export function PropertyPanelFlat({
// Match the legacy Motion gate while preserving TypeScript narrowing. // Match the legacy Motion gate while preserving TypeScript narrowing.
const showMotionTiming = Boolean(sections.timing); const showMotionTiming = Boolean(sections.timing);
const gsapEffectHandlers = const gsapEffectHandlers =
STUDIO_GSAP_PANEL_ENABLED &&
onUpdateGsapProperty && onUpdateGsapProperty &&
onUpdateGsapMeta && onUpdateGsapMeta &&
onDeleteGsapAnimation && onDeleteGsapAnimation &&
@@ -16,35 +16,18 @@ describe("manual editing availability", () => {
vi.resetModules(); vi.resetModules();
}); });
it("enables inspector selection and manual dragging by default", async () => {
const availability = await loadAvailabilityWithEnv({});
expect(availability.STUDIO_PREVIEW_MANUAL_EDITING_ENABLED).toBe(true);
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true);
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true);
});
it("disables preview selection when the inspector panel flag is explicitly off", async () => {
const availability = await loadAvailabilityWithEnv({
VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "0",
});
expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(false);
expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(false);
});
it("enables feature flags with explicit truthy env values", () => { it("enables feature flags with explicit truthy env values", () => {
expect( expect(
resolveStudioBooleanEnvFlag( resolveStudioBooleanEnvFlag(
{ VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING: "true" }, { VITE_STUDIO_ENABLE_FLAT_INSPECTOR: "true" },
["VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING"], ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR"],
false, false,
), ),
).toBe(true); ).toBe(true);
expect( expect(
resolveStudioBooleanEnvFlag( resolveStudioBooleanEnvFlag(
{ VITE_STUDIO_ENABLE_MOTION_PANEL: "1" }, { VITE_STUDIO_SDK_CUTOVER_ENABLED: "1" },
["VITE_STUDIO_ENABLE_MOTION_PANEL"], ["VITE_STUDIO_SDK_CUTOVER_ENABLED"],
false, false,
), ),
).toBe(true); ).toBe(true);
@@ -53,15 +36,15 @@ describe("manual editing availability", () => {
it("disables feature flags with explicit falsy env values", () => { it("disables feature flags with explicit falsy env values", () => {
expect( expect(
resolveStudioBooleanEnvFlag( resolveStudioBooleanEnvFlag(
{ VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING: "off" }, { VITE_STUDIO_ENABLE_FLAT_INSPECTOR: "off" },
["VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING"], ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR"],
true, true,
), ),
).toBe(false); ).toBe(false);
expect( expect(
resolveStudioBooleanEnvFlag( resolveStudioBooleanEnvFlag(
{ VITE_STUDIO_ENABLE_MOTION_PANEL: "0" }, { VITE_STUDIO_SDK_CUTOVER_ENABLED: "0" },
["VITE_STUDIO_ENABLE_MOTION_PANEL"], ["VITE_STUDIO_SDK_CUTOVER_ENABLED"],
true, true,
), ),
).toBe(false); ).toBe(false);
@@ -70,18 +53,8 @@ describe("manual editing availability", () => {
it("supports legacy flag aliases after the preferred name", () => { it("supports legacy flag aliases after the preferred name", () => {
expect( expect(
resolveStudioBooleanEnvFlag( resolveStudioBooleanEnvFlag(
{ VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED: "yes" }, { VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "yes" },
[ ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
"VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING",
"VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED",
],
false,
),
).toBe(true);
expect(
resolveStudioBooleanEnvFlag(
{ VITE_STUDIO_MOTION_PANEL_ENABLED: "enabled" },
["VITE_STUDIO_ENABLE_MOTION_PANEL", "VITE_STUDIO_MOTION_PANEL_ENABLED"],
false, false,
), ),
).toBe(true); ).toBe(true);
@@ -91,10 +64,10 @@ describe("manual editing availability", () => {
expect( expect(
resolveStudioBooleanEnvFlag( resolveStudioBooleanEnvFlag(
{ {
VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "off", VITE_STUDIO_ENABLE_FLAT_INSPECTOR: "off",
VITE_STUDIO_INSPECTOR_PANELS_ENABLED: "on", VITE_STUDIO_FLAT_INSPECTOR_ENABLED: "on",
}, },
["VITE_STUDIO_ENABLE_INSPECTOR_PANELS", "VITE_STUDIO_INSPECTOR_PANELS_ENABLED"], ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
true, true,
), ),
).toBe(false); ).toBe(false);
@@ -1,7 +1,5 @@
export type StudioFeatureFlagEnv = Record<string, boolean | string | undefined>; export type StudioFeatureFlagEnv = Record<string, boolean | string | undefined>;
const STUDIO_PREVIEW_MANUAL_DRAGGING_ENV = "VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING";
const STUDIO_INSPECTOR_PANELS_ENV = "VITE_STUDIO_ENABLE_INSPECTOR_PANELS";
const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on", "enabled"]); const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on", "enabled"]);
const FALSY_ENV_VALUES = new Set(["0", "false", "no", "off", "disabled"]); const FALSY_ENV_VALUES = new Set(["0", "false", "no", "off", "disabled"]);
@@ -40,44 +38,6 @@ const runtimeEnv =
: {}; : {};
const env = { ...(import.meta.env ?? {}), ...runtimeEnv } as StudioFeatureFlagEnv; const env = { ...(import.meta.env ?? {}), ...runtimeEnv } as StudioFeatureFlagEnv;
export const STUDIO_PREVIEW_MANUAL_EDITING_ENABLED = resolveStudioBooleanEnvFlag(
env,
[STUDIO_PREVIEW_MANUAL_DRAGGING_ENV, "VITE_STUDIO_PREVIEW_MANUAL_EDITING_ENABLED"],
true,
);
export const STUDIO_INSPECTOR_PANELS_ENABLED = resolveStudioBooleanEnvFlag(
env,
[STUDIO_INSPECTOR_PANELS_ENV, "VITE_STUDIO_INSPECTOR_PANELS_ENABLED"],
true,
);
export const STUDIO_BLOCKS_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
env,
["VITE_STUDIO_ENABLE_BLOCKS_PANEL", "VITE_STUDIO_BLOCKS_PANEL_ENABLED"],
true,
);
export const STUDIO_GSAP_PANEL_ENABLED = resolveStudioBooleanEnvFlag(
env,
["VITE_STUDIO_ENABLE_GSAP_PANEL", "VITE_STUDIO_GSAP_PANEL_ENABLED"],
true,
);
export const STUDIO_KEYFRAMES_ENABLED = resolveStudioBooleanEnvFlag(
env,
["VITE_STUDIO_ENABLE_KEYFRAMES", "VITE_STUDIO_KEYFRAMES_ENABLED"],
true,
);
export const STUDIO_RAZOR_TOOL_ENABLED = resolveStudioBooleanEnvFlag(
env,
["VITE_STUDIO_ENABLE_RAZOR_TOOL", "VITE_STUDIO_RAZOR_TOOL_ENABLED"],
true,
);
export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED;
// Stage 7 Step 3c: SDK cutover — routes inline-style ops through SDK dispatch // Stage 7 Step 3c: SDK cutover — routes inline-style ops through SDK dispatch
// instead of the server patch-element API. Default false; enable via // instead of the server patch-element API. Default false; enable via
// VITE_STUDIO_SDK_CUTOVER_ENABLED=true. Requires SDK session to be open. // VITE_STUDIO_SDK_CUTOVER_ENABLED=true. Requires SDK session to be open.
@@ -114,5 +74,4 @@ export const STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
true, true,
); );
export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
import { resolveEnabledSdkFamilies } from "../../utils/sdkCutoverPolicy"; import { resolveEnabledSdkFamilies } from "../../utils/sdkCutoverPolicy";
@@ -1,6 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import type { DomEditSelection } from "./domEditingTypes"; import type { DomEditSelection } from "./domEditingTypes";
import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
import { MetricField } from "./propertyPanelPrimitives"; import { MetricField } from "./propertyPanelPrimitives";
import { KeyframeNavigation } from "./KeyframeNavigation"; import { KeyframeNavigation } from "./KeyframeNavigation";
import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers"; import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
@@ -259,7 +258,7 @@ function Transform3dField({
}} }}
/> />
</div> </div>
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && ( {(gsapAnimId || onCommitAnimatedProperty) && (
<KeyframeNavigation <KeyframeNavigation
property={prop} property={prop}
keyframes={ctx.gsapKeyframes} keyframes={ctx.gsapKeyframes}
@@ -2,7 +2,6 @@ import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives"; import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives";
import { KeyframeNavigation } from "./KeyframeNavigation"; import { KeyframeNavigation } from "./KeyframeNavigation";
import { formatPxMetricValue } from "./propertyPanelHelpers"; import { formatPxMetricValue } from "./propertyPanelHelpers";
import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
import { resolveValueTier } from "./propertyPanelValueTier"; import { resolveValueTier } from "./propertyPanelValueTier";
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform"; import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
import type { DomEditSelection } from "./domEditingTypes"; import type { DomEditSelection } from "./domEditingTypes";
@@ -69,7 +68,7 @@ function KeyframeGutter({
| "onConvertToKeyframes" | "onConvertToKeyframes"
>) { >) {
const track = useTrackDesignInput(); const track = useTrackDesignInput();
if (!STUDIO_KEYFRAMES_ENABLED || !gsapAnimId) return null; if (!gsapAnimId) return null;
const hasKeyframesOnProp = Boolean(navKeyframes?.some((kf) => property in kf.properties)); const hasKeyframesOnProp = Boolean(navKeyframes?.some((kf) => property in kf.properties));
return ( return (
<span data-flat-kf-gutter="true" style={{ opacity: hasKeyframesOnProp ? 1 : 0.3 }}> <span data-flat-kf-gutter="true" style={{ opacity: hasKeyframesOnProp ? 1 : 0.3 }}>
@@ -4,12 +4,6 @@ import { DomEditOverlay } from "../editor/DomEditOverlay";
import { MotionPathOverlay } from "../editor/MotionPathOverlay"; import { MotionPathOverlay } from "../editor/MotionPathOverlay";
import { SnapToolbar } from "../editor/SnapToolbar"; import { SnapToolbar } from "../editor/SnapToolbar";
import { useCompositionDimensions } from "../../hooks/useCompositionDimensions"; import { useCompositionDimensions } from "../../hooks/useCompositionDimensions";
import {
STUDIO_INSPECTOR_PANELS_ENABLED,
STUDIO_KEYFRAMES_ENABLED,
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
STUDIO_PREVIEW_SELECTION_ENABLED,
} from "../editor/manualEditingAvailability";
import { useStudioPlaybackContext, useStudioShellContext } from "../../contexts/StudioContext"; import { useStudioPlaybackContext, useStudioShellContext } from "../../contexts/StudioContext";
import { import {
useDomEditActionsContext, useDomEditActionsContext,
@@ -203,21 +197,17 @@ export function PreviewOverlays({
return <CaptionOverlay iframeRef={previewIframeRef} />; return <CaptionOverlay iframeRef={previewIframeRef} />;
} }
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return null;
return ( return (
<> <>
<DomEditOverlay <DomEditOverlay
iframeRef={previewIframeRef} iframeRef={previewIframeRef}
activeCompositionPath={activeCompPath} activeCompositionPath={activeCompPath}
hoverSelection={ hoverSelection={
STUDIO_PREVIEW_SELECTION_ENABLED && !captionEditMode && !compositionLoading && !isPlaying !captionEditMode && !compositionLoading && !isPlaying ? domEditHoverSelection : null
? domEditHoverSelection
: null
} }
selection={shouldShowSelectedDomBounds ? domEditSelection : null} selection={shouldShowSelectedDomBounds ? domEditSelection : null}
groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []} groupSelections={shouldShowSelectedDomBounds ? domEditGroupSelections : []}
allowCanvasMovement={STUDIO_PREVIEW_MANUAL_EDITING_ENABLED && !isGestureRecording} allowCanvasMovement={!isGestureRecording}
onCanvasMouseDown={handlePreviewCanvasMouseDown} onCanvasMouseDown={handlePreviewCanvasMouseDown}
onCanvasPointerMove={handlePreviewCanvasPointerMove} onCanvasPointerMove={handlePreviewCanvasPointerMove}
onCanvasPointerLeave={handlePreviewCanvasPointerLeave} onCanvasPointerLeave={handlePreviewCanvasPointerLeave}
@@ -273,14 +263,12 @@ export function PreviewOverlays({
onMarqueeSelect={applyMarqueeSelection} onMarqueeSelect={applyMarqueeSelection}
/> />
<SnapToolbar onSnapChange={setSnapPrefs} /> <SnapToolbar onSnapChange={setSnapPrefs} />
{STUDIO_KEYFRAMES_ENABLED && ( <MotionPathOverlay
<MotionPathOverlay iframeRef={previewIframeRef}
iframeRef={previewIframeRef} selection={shouldShowMotionPath ? domEditSelection : null}
selection={shouldShowMotionPath ? domEditSelection : null} compositionSize={compositionDimensions}
compositionSize={compositionDimensions} isPlaying={isPlaying}
isPlaying={isPlaying} />
/>
)}
{gestureOverlay} {gestureOverlay}
</> </>
); );
@@ -12,7 +12,6 @@ import { AssetsTab } from "./AssetsTab";
import { trackStudioEvent } from "../../utils/studioTelemetry"; import { trackStudioEvent } from "../../utils/studioTelemetry";
import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab"; import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab";
import { FileTree } from "../editor/FileTree"; import { FileTree } from "../editor/FileTree";
import { STUDIO_BLOCKS_PANEL_ENABLED } from "../editor/manualEditingAvailability";
import { Tooltip } from "../ui"; import { Tooltip } from "../ui";
export type SidebarTab = "compositions" | "assets" | "code" | "blocks"; export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
@@ -127,11 +126,7 @@ export const LeftSidebar = memo(
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
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)]" 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)]"
style={{ style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}
gridTemplateColumns: STUDIO_BLOCKS_PANEL_ENABLED
? "1fr 1fr 1fr 1fr"
: "1fr 1fr 1fr",
}}
> >
<Tooltip label="Source code editor" side="bottom"> <Tooltip label="Source code editor" side="bottom">
<button <button
@@ -172,21 +167,19 @@ export const LeftSidebar = memo(
Assets Assets
</button> </button>
</Tooltip> </Tooltip>
{STUDIO_BLOCKS_PANEL_ENABLED && ( <Tooltip label="Browse blocks and components" side="bottom">
<Tooltip label="Browse blocks and components" side="bottom"> <button
<button type="button"
type="button" onClick={() => selectTab("blocks")}
onClick={() => selectTab("blocks")} className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${ tab === "blocks"
tab === "blocks" ? "bg-neutral-800 text-white"
? "bg-neutral-800 text-white" : "text-neutral-500 hover:text-neutral-200"
: "text-neutral-500 hover:text-neutral-200" }`}
}`} >
> Catalog
Catalog </button>
</button> </Tooltip>
</Tooltip>
)}
</div> </div>
{onToggleCollapse && ( {onToggleCollapse && (
<button <button
@@ -267,7 +260,7 @@ export const LeftSidebar = memo(
</div> </div>
)} )}
{STUDIO_BLOCKS_PANEL_ENABLED && tab === "blocks" && ( {tab === "blocks" && (
<BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} /> <BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
)} )}
+1 -2
View File
@@ -7,7 +7,6 @@ import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
import { isEditableTarget } from "../utils/timelineDiscovery"; import { isEditableTarget } from "../utils/timelineDiscovery";
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers"; import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
import { canSplitElement } from "../utils/timelineElementSplit"; import { canSplitElement } from "../utils/timelineElementSplit";
import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
import { trackStudioEvent } from "../utils/studioTelemetry"; import { trackStudioEvent } from "../utils/studioTelemetry";
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator"; import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
@@ -258,7 +257,7 @@ function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks
} }
} }
if (STUDIO_RAZOR_TOOL_ENABLED && key === "b" && !event.shiftKey && !event.altKey) { if (key === "b" && !event.shiftKey && !event.altKey) {
event.preventDefault(); event.preventDefault();
const { activeTool, setActiveTool } = usePlayerStore.getState(); const { activeTool, setActiveTool } = usePlayerStore.getState();
setActiveTool(activeTool === "razor" ? "select" : "razor"); setActiveTool(activeTool === "razor" ? "select" : "razor");
@@ -4,7 +4,6 @@
* Extracted from useDomEditSession to keep file sizes under the 600-line limit. * Extracted from useDomEditSession to keep file sizes under the 600-line limit.
*/ */
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing"; import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits"; import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
import type { SidebarTab } from "../components/sidebar/LeftSidebar"; import type { SidebarTab } from "../components/sidebar/LeftSidebar";
@@ -53,7 +52,7 @@ export function useDomEditPreviewSync({
// fallow-ignore-next-line complexity // fallow-ignore-next-line complexity
const syncSelectionFromDocument = async () => { const syncSelectionFromDocument = async () => {
if (!STUDIO_INSPECTOR_PANELS_ENABLED || captionEditMode) return; if (captionEditMode) return;
const currentSelection = domEditSelectionRef.current; const currentSelection = domEditSelectionRef.current;
if (!currentSelection) return; if (!currentSelection) return;
let doc: Document | null = null; let doc: Document | null = null;
@@ -9,7 +9,6 @@
*/ */
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import type { DomEditSelection } from "../components/editor/domEditingTypes"; import type { DomEditSelection } from "../components/editor/domEditingTypes";
import { STUDIO_GSAP_PANEL_ENABLED } from "../components/editor/manualEditingAvailability";
import { usePlayerStore } from "../player"; import { usePlayerStore } from "../player";
import { useDomEditPreviewSync } from "./useDomEditPreviewSync"; import { useDomEditPreviewSync } from "./useDomEditPreviewSync";
import { useGsapAnimationsForElement, usePopulateKeyframeCacheForFile } from "./useGsapTweenCache"; import { useGsapAnimationsForElement, usePopulateKeyframeCacheForFile } from "./useGsapTweenCache";
@@ -195,7 +194,7 @@ export function useDomEditWiring({
const gsapSourceFile = domEditSelection?.sourceFile || activeCompPath || "index.html"; const gsapSourceFile = domEditSelection?.sourceFile || activeCompPath || "index.html";
usePopulateKeyframeCacheForFile( usePopulateKeyframeCacheForFile(
STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null, projectId ?? null,
gsapSourceFile, gsapSourceFile,
gsapCacheVersion, gsapCacheVersion,
previewIframeRef, previewIframeRef,
@@ -206,7 +205,7 @@ export function useDomEditWiring({
multipleTimelines: gsapMultipleTimelines, multipleTimelines: gsapMultipleTimelines,
unsupportedTimelinePattern: gsapUnsupportedTimelinePattern, unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
} = useGsapAnimationsForElement( } = useGsapAnimationsForElement(
STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null, projectId ?? null,
gsapSourceFile, gsapSourceFile,
domEditSelection domEditSelection
? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null } ? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null }
@@ -16,7 +16,6 @@ import {
replaceDomEditGroupSelection, replaceDomEditGroupSelection,
seedDomEditGroupWithSelection, seedDomEditGroupWithSelection,
} from "../utils/domEditHelpers"; } from "../utils/domEditHelpers";
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
import { import {
findElementForSelection, findElementForSelection,
findElementForTimelineElement, findElementForTimelineElement,
@@ -164,14 +163,6 @@ export function useDomSelection({
setSelectedTimelineElementId(null); setSelectedTimelineElementId(null);
return; return;
} }
if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
domEditSelectionRef.current = null;
domEditGroupSelectionsRef.current = [];
setDomEditSelection(null);
setDomEditGroupSelections([]);
setSelectedTimelineElementId(null);
return;
}
const isAdditiveSelection = Boolean(options?.additive); const isAdditiveSelection = Boolean(options?.additive);
const currentSelection = domEditSelectionRef.current; const currentSelection = domEditSelectionRef.current;
@@ -370,7 +361,6 @@ export function useDomSelection({
const handleTimelineElementSelect = useCallback( const handleTimelineElementSelect = useCallback(
async (element: TimelineElement | null) => { async (element: TimelineElement | null) => {
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
const seq = ++timelineSelectSeqRef.current; const seq = ++timelineSelectSeqRef.current;
if (!element) { if (!element) {
applyDomSelection(null, { revealPanel: false }); applyDomSelection(null, { revealPanel: false });
@@ -513,14 +503,6 @@ export function useDomSelection({
const applyMarqueeSelection = useCallback( const applyMarqueeSelection = useCallback(
// fallow-ignore-next-line complexity // fallow-ignore-next-line complexity
(selections: DomEditSelection[], additive: boolean) => { (selections: DomEditSelection[], additive: boolean) => {
// Honor the inspector-panels kill switch like applyDomSelection does.
if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
domEditSelectionRef.current = null;
domEditGroupSelectionsRef.current = [];
setDomEditSelection(null);
setDomEditGroupSelections([]);
return;
}
if (selections.length === 0) { if (selections.length === 0) {
if (!additive) applyDomSelection(null, { revealPanel: false }); if (!additive) applyDomSelection(null, { revealPanel: false });
return; return;
@@ -557,15 +539,6 @@ export function useDomSelection({
[applyDomSelection, timelineElements, setSelectedTimelineElementId], [applyDomSelection, timelineElements, setSelectedTimelineElementId],
); );
// Disabled inspector effect
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (STUDIO_INSPECTOR_PANELS_ENABLED) return;
updateDomEditHoverSelection(null);
applyDomSelection(null, { revealPanel: false });
if (rightPanelTab !== "renders") setRightPanelTab("renders");
}, [applyDomSelection, rightPanelTab, updateDomEditHoverSelection, setRightPanelTab]);
return { return {
// State // State
domEditSelection, domEditSelection,
@@ -1,7 +1,6 @@
import { useCallback, useRef } from "react"; import { useCallback, useRef } from "react";
import { liveTime, usePlayerStore } from "../player"; import { liveTime, usePlayerStore } from "../player";
import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers"; import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers";
import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability";
import { type DomEditSelection } from "../components/editor/domEditing"; import { type DomEditSelection } from "../components/editor/domEditing";
import type { ApplyDomSelectionOptions, ResolveDomSelectionOptions } from "./useDomSelection"; import type { ApplyDomSelectionOptions, ResolveDomSelectionOptions } from "./useDomSelection";
import { trackStudioEvent } from "../utils/studioTelemetry"; import { trackStudioEvent } from "../utils/studioTelemetry";
@@ -87,7 +86,7 @@ export function usePreviewInteraction({
const handlePreviewCanvasMouseDown = useCallback( const handlePreviewCanvasMouseDown = useCallback(
// fallow-ignore-next-line complexity // fallow-ignore-next-line complexity
async (e: React.MouseEvent<HTMLDivElement>, options?: PreviewMouseDownOptions) => { async (e: React.MouseEvent<HTMLDivElement>, options?: PreviewMouseDownOptions) => {
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) return; if (captionEditMode || compositionLoading) return;
// Manual double-click detection (see DOUBLE_CLICK_MS): the first click // Manual double-click detection (see DOUBLE_CLICK_MS): the first click
// re-renders the overlay so `e.detail` never reaches 2 on the canvas. // re-renders the overlay so `e.detail` never reaches 2 on the canvas.
@@ -236,7 +235,7 @@ export function usePreviewInteraction({
const handlePreviewCanvasPointerMove = useCallback( const handlePreviewCanvasPointerMove = useCallback(
// fallow-ignore-next-line complexity // fallow-ignore-next-line complexity
async (e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => { async (e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
if (!STUDIO_PREVIEW_SELECTION_ENABLED || captionEditMode || compositionLoading) { if (captionEditMode || compositionLoading) {
updateDomEditHoverSelection(null); updateDomEditHoverSelection(null);
return null; return null;
} }
@@ -1,5 +1,4 @@
import { useCallback, useMemo, useRef, useState, type DragEvent } from "react"; import { useCallback, useMemo, useRef, useState, type DragEvent } from "react";
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
import type { DomEditSelection } from "../components/editor/domEditing"; import type { DomEditSelection } from "../components/editor/domEditing";
import type { StudioContextValue } from "../contexts/StudioContext"; import type { StudioContextValue } from "../contexts/StudioContext";
import type { RightInspectorPanes } from "../utils/studioHelpers"; import type { RightInspectorPanes } from "../utils/studioHelpers";
@@ -85,17 +84,14 @@ export function useInspectorState(
// fallow-ignore-next-line complexity // fallow-ignore-next-line complexity
return useMemo(() => { return useMemo(() => {
const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers"; const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
const layersPanelActive = const layersPanelActive = inspectorTabActive && rightInspectorPanes.layers;
STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.layers; const designPanelActive = inspectorTabActive && rightInspectorPanes.design;
const designPanelActive =
STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.design;
const inspectorPanelActive = layersPanelActive || designPanelActive; const inspectorPanelActive = layersPanelActive || designPanelActive;
return { return {
layersPanelActive, layersPanelActive,
designPanelActive, designPanelActive,
inspectorPanelActive, inspectorPanelActive,
inspectorButtonActive: inspectorButtonActive: !rightCollapsed && inspectorPanelActive,
STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive,
// Deliberately wider than shouldShowSelectedDomBounds: the on-canvas path // Deliberately wider than shouldShowSelectedDomBounds: the on-canvas path
// handles ARE the arc-drag affordance, so gating them on an open Inspector // handles ARE the arc-drag affordance, so gating them on an open Inspector
// would make keyframe path editing reachable only from a side panel. // would make keyframe path editing reachable only from a side panel.
@@ -32,7 +32,6 @@ import {
useTimelineTrackLayout, useTimelineTrackLayout,
} from "./useTimelineTrackLayout"; } from "./useTimelineTrackLayout";
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers"; import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
import { useTrackGapMenu } from "./useTrackGapMenu"; import { useTrackGapMenu } from "./useTrackGapMenu";
import { useTimelineGapHighlights } from "./useTimelineGapHighlights"; import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext"; import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext";
@@ -126,7 +125,7 @@ export const Timeline = memo(function Timeline({
), ),
[gsapAnimations], [gsapAnimations],
); );
const labelMode = STUDIO_KEYFRAMES_ENABLED && hasKeyframedClips; const labelMode = hasKeyframedClips;
// Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD // Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD
// (dropping it would jam clip 0 against the gutter on every non-keyframed // (dropping it would jam clip 0 against the gutter on every non-keyframed
// composition); in label mode the 232px label column already provides it. // composition); in label mode the 232px label column already provides it.
@@ -17,7 +17,6 @@ import {
} from "./timelineMultiDragPreview"; } from "./timelineMultiDragPreview";
import type { TimelineLaneBaseProps } from "./timelineLaneProps"; import type { TimelineLaneBaseProps } from "./timelineLaneProps";
import type { TimelineEditCallbacks } from "./timelineCallbacks"; import type { TimelineEditCallbacks } from "./timelineCallbacks";
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
import { trackStudioKeyframeLaneExpand } from "../../telemetry/events"; import { trackStudioKeyframeLaneExpand } from "../../telemetry/events";
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit"; import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector"; import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
@@ -134,9 +133,12 @@ export function TimelineLanes({
// The one keyframed element this track shows lanes for (selected, else // The one keyframed element this track shows lanes for (selected, else
// most lanes). A track can hold several elements; scoping to one keeps // most lanes). A track can hold several elements; scoping to one keeps
// their keyframes from cramming into a single row. // their keyframes from cramming into a single row.
const keyframeClip = STUDIO_KEYFRAMES_ENABLED const keyframeClip = resolveTrackKeyframeClip(
? resolveTrackKeyframeClip(els, laneCounts, selectedElementId, selectedElementIds) els,
: null; laneCounts,
selectedElementId,
selectedElementIds,
);
const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id; const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
const keyframeClipExpanded = const keyframeClipExpanded =
keyframeClipKey != null && expandedClipIds.has(keyframeClipKey); keyframeClipKey != null && expandedClipIds.has(keyframeClipKey);
@@ -249,8 +251,7 @@ export function TimelineLanes({
// Only the track's active keyframe clip shows expanded lanes; // Only the track's active keyframe clip shows expanded lanes;
// other clips (incl. siblings on a shared track) show compact // other clips (incl. siblings on a shared track) show compact
// diamonds on their own bar instead. // diamonds on their own bar instead.
const isTrackKeyframeClip = const isTrackKeyframeClip = elementKey === keyframeClipKey;
STUDIO_KEYFRAMES_ENABLED && elementKey === keyframeClipKey;
const showsLanes = isTrackKeyframeClip && keyframeClipExpanded; const showsLanes = isTrackKeyframeClip && keyframeClipExpanded;
const capabilities = getTimelineEditCapabilities(el); const capabilities = getTimelineEditCapabilities(el);
const isSelected = const isSelected =
@@ -428,35 +429,32 @@ export function TimelineLanes({
renderClipContent, renderClipContent,
renderClipOverlay, renderClipOverlay,
)} )}
{STUDIO_KEYFRAMES_ENABLED && {!showsLanes && keyframeCache?.get(elementKey) && (
!showsLanes && <TimelineClipDiamonds
keyframeCache?.get(elementKey) && ( keyframesData={keyframeCache.get(elementKey)!}
<TimelineClipDiamonds clipWidthPx={Math.max(previewElement.duration * pps, 4)}
keyframesData={keyframeCache.get(elementKey)!} clipHeightPx={rowHeight - 2 * CLIP_Y}
clipWidthPx={Math.max(previewElement.duration * pps, 4)} beatsActive={beatStripOnTrack}
clipHeightPx={rowHeight - 2 * CLIP_Y} accentColor={clipStyle.accent}
beatsActive={beatStripOnTrack} isSelected={isSelected}
accentColor={clipStyle.accent} currentPercentage={
isSelected={isSelected} previewElement.duration > 0
currentPercentage={ ? ((currentTime - previewElement.start) / previewElement.duration) *
previewElement.duration > 0 100
? ((currentTime - previewElement.start) / : 0
previewElement.duration) * }
100 elementId={elementKey}
: 0 selectedKeyframes={selectedKeyframes}
} onClickKeyframe={(_elId, target) =>
elementId={elementKey} onClickKeyframe?.(previewElement, target)
selectedKeyframes={selectedKeyframes} }
onClickKeyframe={(_elId, target) => onShiftClickKeyframe={onShiftClickKeyframe}
onClickKeyframe?.(previewElement, target) onContextMenuKeyframe={onContextMenuKeyframe}
} onMoveKeyframe={onMoveKeyframe}
onShiftClickKeyframe={onShiftClickKeyframe} onSelectSegment={onSelectSegment}
onContextMenuKeyframe={onContextMenuKeyframe} suppressClickRef={suppressClickRef}
onMoveKeyframe={onMoveKeyframe} />
onSelectSegment={onSelectSegment} )}
suppressClickRef={suppressClickRef}
/>
)}
</TimelineClip> </TimelineClip>
); );
// Mounted for the track's keyframe clip in BOTH disclosure // Mounted for the track's keyframe clip in BOTH disclosure
@@ -1,7 +1,6 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { usePlayerStore } from "../store/playerStore"; import { usePlayerStore } from "../store/playerStore";
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
import { useStudioShellContextOptional } from "../../contexts/StudioContext"; import { useStudioShellContextOptional } from "../../contexts/StudioContext";
import { animationContributesLane } from "./TimelinePropertyLanes"; import { animationContributesLane } from "./TimelinePropertyLanes";
@@ -34,7 +33,6 @@ export function useAutoExpandKeyframedClips(gsapAnimations: Map<string, GsapAnim
const projectId = useStudioShellContextOptional()?.projectId ?? null; const projectId = useStudioShellContextOptional()?.projectId ?? null;
const seen = useRef({ projectId, source: gsapAnimations, clips: new Set<string>() }); const seen = useRef({ projectId, source: gsapAnimations, clips: new Set<string>() });
useEffect(() => { useEffect(() => {
if (!STUDIO_KEYFRAMES_ENABLED) return;
if (seen.current.projectId !== projectId) { if (seen.current.projectId !== projectId) {
const sourceChanged = seen.current.source !== gsapAnimations; const sourceChanged = seen.current.source !== gsapAnimations;
seen.current = { projectId, source: gsapAnimations, clips: new Set() }; seen.current = { projectId, source: gsapAnimations, clips: new Set() };
@@ -2,7 +2,6 @@ import { useMemo, useRef } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { animationLaneGroups } from "./TimelinePropertyLanes"; import { animationLaneGroups } from "./TimelinePropertyLanes";
import { usePlayerStore, type TimelineElement } from "../store/playerStore"; import { usePlayerStore, type TimelineElement } from "../store/playerStore";
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
import type { DraggedClipState } from "./timelineClipDragTypes"; import type { DraggedClipState } from "./timelineClipDragTypes";
import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations"; import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
import { import {
@@ -90,10 +89,7 @@ function useTimelineRowHeights(
}); });
return { return {
laneCounts, laneCounts,
rowHeights: trackHeights( rowHeights: trackHeights(heightTracks, expandedClipIds),
heightTracks,
STUDIO_KEYFRAMES_ENABLED ? expandedClipIds : undefined,
),
}; };
}, [expandedClipIds, gsapAnimations, tracks, selectedElementId, selectedElementIds]); }, [expandedClipIds, gsapAnimations, tracks, selectedElementId, selectedElementIds]);
const rowHeightsRef = useRef<readonly number[]>(rowHeights); const rowHeightsRef = useRef<readonly number[]>(rowHeights);
@@ -34,12 +34,8 @@ describe("resolveMasterCompositionPath", () => {
describe("normalizeStudioUrlPanelTab", () => { describe("normalizeStudioUrlPanelTab", () => {
it("accepts slideshow and variables as valid tabs", () => { it("accepts slideshow and variables as valid tabs", () => {
expect(normalizeStudioUrlPanelTab("slideshow", { inspectorPanelsEnabled: true })).toBe( expect(normalizeStudioUrlPanelTab("slideshow")).toBe("slideshow");
"slideshow", expect(normalizeStudioUrlPanelTab("variables")).toBe("variables");
);
expect(normalizeStudioUrlPanelTab("variables", { inspectorPanelsEnabled: true })).toBe(
"variables",
);
}); });
}); });
@@ -184,9 +180,10 @@ describe("studio url state", () => {
).toBe("compositions/title.html"); ).toBe("compositions/title.html");
}); });
it("normalizes url tabs against feature flags", () => { it("passes through every valid tab and rejects unknown ones", () => {
expect(normalizeStudioUrlPanelTab("renders")).toBe("renders"); expect(normalizeStudioUrlPanelTab("renders")).toBe("renders");
expect(normalizeStudioUrlPanelTab("layers", { inspectorPanelsEnabled: false })).toBe("renders"); expect(normalizeStudioUrlPanelTab("layers")).toBe("layers");
expect(normalizeStudioUrlPanelTab("nope" as never)).toBeNull();
}); });
it("hydrates seek first, preserves the initial url state, then restores selection", async () => { it("hydrates seek first, preserves the initial url state, then restores selection", async () => {
+1 -10
View File
@@ -1,6 +1,5 @@
import type { RightPanelTab } from "./studioHelpers"; import type { RightPanelTab } from "./studioHelpers";
import { buildProjectHash, parseProjectHashRoute } from "./projectRouting"; import { buildProjectHash, parseProjectHashRoute } from "./projectRouting";
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
import { roundTo3 } from "./rounding"; import { roundTo3 } from "./rounding";
export interface StudioUrlSelectionState { export interface StudioUrlSelectionState {
@@ -34,17 +33,9 @@ export function resolveMasterCompositionPath(fileTree: string[]): string | null
return fileTree.find((p) => p.endsWith(".html")) ?? null; return fileTree.find((p) => p.endsWith(".html")) ?? null;
} }
export function normalizeStudioUrlPanelTab( export function normalizeStudioUrlPanelTab(tab: RightPanelTab | null): RightPanelTab | null {
tab: RightPanelTab | null,
options: {
inspectorPanelsEnabled?: boolean;
} = {},
): RightPanelTab | null {
if (!tab) return null; if (!tab) return null;
if (!VALID_TABS.includes(tab)) return null; if (!VALID_TABS.includes(tab)) return null;
const inspectorPanelsEnabled = options.inspectorPanelsEnabled ?? STUDIO_INSPECTOR_PANELS_ENABLED;
if (!inspectorPanelsEnabled && tab !== "renders") return "renders";
return tab; return tab;
} }