mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 01:56:04 +00:00
## 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.
278 lines
8.9 KiB
TypeScript
278 lines
8.9 KiB
TypeScript
/**
|
|
* Wiring layer for DOM edit sessions: click-to-source navigation,
|
|
* DOM selection to timeline sync, GSAP cache invalidation on refresh,
|
|
* GSAP cache population, animation resolution for the selected element,
|
|
* and preview sync side-effects.
|
|
*
|
|
* Extracted from useDomEditSession to isolate orchestration wiring from
|
|
* the GSAP-aware geometry intercept logic.
|
|
*/
|
|
import { useCallback, useEffect, useRef } from "react";
|
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
import { usePlayerStore } from "../player";
|
|
import { useDomEditPreviewSync } from "./useDomEditPreviewSync";
|
|
import { useGsapAnimationsForElement, usePopulateKeyframeCacheForFile } from "./useGsapTweenCache";
|
|
import { useGsapAnimationFetchFallback } from "./useGsapAnimationFetchFallback";
|
|
import { useGsapInteractionFailureTelemetry } from "./useGsapInteractionFailureTelemetry";
|
|
import { useGsapSelectionHandlers } from "./useGsapSelectionHandlers";
|
|
import type { PatchTarget } from "../utils/sourcePatcher";
|
|
import type { SidebarTab } from "../components/sidebar/LeftSidebar";
|
|
|
|
export interface UseDomEditWiringParams {
|
|
projectId: string | null;
|
|
activeCompPath: string | null;
|
|
domEditSelection: DomEditSelection | null;
|
|
domEditSelectionRef: React.MutableRefObject<DomEditSelection | null>;
|
|
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
previewIframe: HTMLIFrameElement | null;
|
|
captionEditMode: boolean;
|
|
refreshKey: number;
|
|
gsapCacheVersion: number;
|
|
bumpGsapCache: () => void;
|
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
refreshPreviewDocumentVersion: () => void;
|
|
syncPreviewHistoryHotkey: (iframe: HTMLIFrameElement | null) => void;
|
|
applyStudioManualEditsToPreviewRef: React.MutableRefObject<
|
|
(iframe: HTMLIFrameElement) => Promise<void>
|
|
>;
|
|
applyDomSelection: (
|
|
selection: DomEditSelection | null,
|
|
options?: { revealPanel?: boolean; preserveGroup?: boolean },
|
|
) => void;
|
|
buildDomSelectionFromTarget: (element: HTMLElement) => Promise<DomEditSelection | null>;
|
|
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
|
|
selectSidebarTab?: (tab: SidebarTab) => void;
|
|
getSidebarTab?: () => SidebarTab;
|
|
// GSAP script commit ops (from useGsapScriptCommits)
|
|
updateGsapProperty: (
|
|
sel: DomEditSelection,
|
|
animId: string,
|
|
prop: string,
|
|
value: number | string,
|
|
) => void;
|
|
updateGsapMeta: (
|
|
sel: DomEditSelection,
|
|
animId: string,
|
|
updates: { duration?: number; ease?: string; position?: number },
|
|
) => Promise<void>;
|
|
deleteGsapAnimation: (sel: DomEditSelection, animId: string) => Promise<void>;
|
|
deleteAllForSelector: (sel: DomEditSelection, targetSelector: string) => Promise<void>;
|
|
addGsapAnimation: (
|
|
sel: DomEditSelection,
|
|
method: "to" | "from" | "set" | "fromTo",
|
|
time: number,
|
|
) => Promise<void>;
|
|
addGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
removeGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
updateGsapFromProperty: (
|
|
sel: DomEditSelection,
|
|
animId: string,
|
|
prop: string,
|
|
value: number | string,
|
|
) => Promise<void>;
|
|
addGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
removeGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
addKeyframe: (
|
|
sel: DomEditSelection,
|
|
animId: string,
|
|
percentage: number,
|
|
property: string,
|
|
value: number | string,
|
|
) => void;
|
|
addKeyframeBatch: (
|
|
sel: DomEditSelection,
|
|
animId: string,
|
|
percentage: number,
|
|
properties: Record<string, number | string>,
|
|
) => Promise<void>;
|
|
removeKeyframe: (sel: DomEditSelection, animId: string, percentage: number) => void;
|
|
moveKeyframe: (
|
|
sel: DomEditSelection,
|
|
animId: string,
|
|
fromPercentage: number,
|
|
toPercentage: number,
|
|
) => Promise<boolean>;
|
|
resizeKeyframedTween: (
|
|
sel: DomEditSelection,
|
|
animId: string,
|
|
position: number,
|
|
duration: number,
|
|
pctRemap: Array<{ from: number; to: number }>,
|
|
) => Promise<boolean>;
|
|
convertToKeyframes: (
|
|
sel: DomEditSelection,
|
|
animId: string,
|
|
resolvedFromValues?: Record<string, number | string>,
|
|
) => Promise<void>;
|
|
removeAllKeyframes: (sel: DomEditSelection, animId: string) => Promise<void>;
|
|
handleDomManualEditsReset: (sel: DomEditSelection) => void;
|
|
}
|
|
|
|
// fallow-ignore-next-line complexity
|
|
export function useDomEditWiring({
|
|
// fallow-ignore-next-line code-duplication
|
|
projectId,
|
|
activeCompPath,
|
|
domEditSelection,
|
|
domEditSelectionRef,
|
|
previewIframeRef,
|
|
previewIframe,
|
|
captionEditMode,
|
|
refreshKey,
|
|
gsapCacheVersion,
|
|
bumpGsapCache,
|
|
showToast,
|
|
refreshPreviewDocumentVersion,
|
|
syncPreviewHistoryHotkey,
|
|
applyStudioManualEditsToPreviewRef,
|
|
applyDomSelection,
|
|
buildDomSelectionFromTarget,
|
|
openSourceForSelection,
|
|
selectSidebarTab,
|
|
getSidebarTab,
|
|
updateGsapProperty,
|
|
updateGsapMeta,
|
|
deleteGsapAnimation,
|
|
deleteAllForSelector,
|
|
addGsapAnimation,
|
|
addGsapProperty,
|
|
removeGsapProperty,
|
|
updateGsapFromProperty,
|
|
addGsapFromProperty,
|
|
removeGsapFromProperty,
|
|
addKeyframe,
|
|
addKeyframeBatch,
|
|
removeKeyframe,
|
|
moveKeyframe,
|
|
resizeKeyframedTween,
|
|
convertToKeyframes,
|
|
removeAllKeyframes,
|
|
handleDomManualEditsReset,
|
|
}: UseDomEditWiringParams) {
|
|
// ── Click-to-source navigation ──
|
|
|
|
const onClickToSource = useCallback(
|
|
(selection: DomEditSelection) => {
|
|
if (!openSourceForSelection || !selectSidebarTab) return;
|
|
if (!selection.sourceFile) return;
|
|
selectSidebarTab("code");
|
|
openSourceForSelection(selection.sourceFile, {
|
|
id: selection.id,
|
|
selector: selection.selector,
|
|
selectorIndex: selection.selectorIndex,
|
|
});
|
|
},
|
|
[openSourceForSelection, selectSidebarTab],
|
|
);
|
|
|
|
// ── DOM selection -> timeline element sync ──
|
|
|
|
useEffect(() => {
|
|
if (!domEditSelection?.id) return;
|
|
const { selectedElementId, elements, setSelectedElementId } = usePlayerStore.getState();
|
|
const matchKey = elements.find(
|
|
(el) => el.domId === domEditSelection.id || el.id === domEditSelection.id,
|
|
);
|
|
const key = matchKey ? (matchKey.key ?? matchKey.id) : null;
|
|
if (key && key !== selectedElementId) setSelectedElementId(key);
|
|
}, [domEditSelection?.id]);
|
|
|
|
// ── GSAP cache sync ──
|
|
|
|
// Bump GSAP cache when refreshKey changes (code-tab edits trigger iframe
|
|
// reload via refreshKey but don't go through commitMutation, so the cache
|
|
// would otherwise retain stale keyframe entries).
|
|
const prevRefreshKeyRef = useRef(refreshKey);
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
useEffect(() => {
|
|
if (refreshKey !== prevRefreshKeyRef.current) {
|
|
prevRefreshKeyRef.current = refreshKey;
|
|
bumpGsapCache();
|
|
}
|
|
}, [refreshKey, bumpGsapCache]);
|
|
|
|
const gsapSourceFile = domEditSelection?.sourceFile || activeCompPath || "index.html";
|
|
|
|
usePopulateKeyframeCacheForFile(
|
|
projectId ?? null,
|
|
gsapSourceFile,
|
|
gsapCacheVersion,
|
|
previewIframeRef,
|
|
);
|
|
|
|
const {
|
|
animations: selectedGsapAnimations,
|
|
multipleTimelines: gsapMultipleTimelines,
|
|
unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
|
|
} = useGsapAnimationsForElement(
|
|
projectId ?? null,
|
|
gsapSourceFile,
|
|
domEditSelection
|
|
? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null }
|
|
: null,
|
|
gsapCacheVersion,
|
|
// Pass the preview iframe so class/selector tweens (e.g. `.dot`) resolve to
|
|
// the live element and surface in the inspector — not just by #id match.
|
|
previewIframeRef,
|
|
);
|
|
|
|
// ── Telemetry & fallback ──
|
|
|
|
const trackGsapInteractionFailure = useGsapInteractionFailureTelemetry(activeCompPath, showToast);
|
|
const makeFetchFallback = useGsapAnimationFetchFallback(projectId, gsapSourceFile);
|
|
|
|
// ── GSAP selection handlers ──
|
|
|
|
const gsapSelectionHandlers = useGsapSelectionHandlers({
|
|
domEditSelection,
|
|
updateGsapProperty,
|
|
updateGsapMeta,
|
|
deleteGsapAnimation,
|
|
deleteAllForSelector,
|
|
addGsapAnimation,
|
|
addGsapProperty,
|
|
removeGsapProperty,
|
|
updateGsapFromProperty,
|
|
addGsapFromProperty,
|
|
removeGsapFromProperty,
|
|
addKeyframe,
|
|
addKeyframeBatch,
|
|
removeKeyframe,
|
|
moveKeyframe,
|
|
resizeKeyframedTween,
|
|
convertToKeyframes,
|
|
removeAllKeyframes,
|
|
handleDomManualEditsReset,
|
|
selectedGsapAnimations,
|
|
showToast,
|
|
});
|
|
|
|
// ── Preview sync side-effects ──
|
|
|
|
useDomEditPreviewSync({
|
|
previewIframe,
|
|
activeCompPath,
|
|
captionEditMode,
|
|
domEditSelectionRef,
|
|
domEditSelection,
|
|
applyDomSelection,
|
|
buildDomSelectionFromTarget,
|
|
refreshPreviewDocumentVersion,
|
|
syncPreviewHistoryHotkey,
|
|
applyStudioManualEditsToPreviewRef,
|
|
openSourceForSelection,
|
|
getSidebarTab,
|
|
gsapCacheVersion,
|
|
});
|
|
|
|
return {
|
|
onClickToSource,
|
|
selectedGsapAnimations,
|
|
gsapMultipleTimelines,
|
|
gsapUnsupportedTimelinePattern,
|
|
trackGsapInteractionFailure,
|
|
makeFetchFallback,
|
|
...gsapSelectionHandlers,
|
|
};
|
|
}
|