mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
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:
@@ -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"
|
||||||
|
|||||||
@@ -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} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user