mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +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.
383 lines
11 KiB
TypeScript
383 lines
11 KiB
TypeScript
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives";
|
|
import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
import { formatPxMetricValue } from "./propertyPanelHelpers";
|
|
import { resolveValueTier } from "./propertyPanelValueTier";
|
|
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
|
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
|
|
type KeyframeEntry = Array<{
|
|
percentage: number;
|
|
tweenPercentage?: number;
|
|
properties: Record<string, number | string>;
|
|
ease?: string;
|
|
}> | null;
|
|
|
|
interface GeometryRowsProps {
|
|
element: DomEditSelection;
|
|
displayX: number;
|
|
displayY: number;
|
|
displayW: number;
|
|
displayH: number;
|
|
displayR: number;
|
|
manualOffsetEditingDisabled: boolean;
|
|
manualSizeEditingDisabled: boolean;
|
|
manualRotationEditingDisabled: boolean;
|
|
commitManualOffset: (axis: "x" | "y", value: string) => void;
|
|
commitManualSize: (dimension: "width" | "height", value: string) => void;
|
|
commitManualRotation: (value: string) => void;
|
|
gsapAnimId: string | null;
|
|
navKeyframes: KeyframeEntry;
|
|
currentPct: number;
|
|
seekFromKfPct: (pct: number) => void;
|
|
animIdForProp: (prop: string) => string;
|
|
onCommitAnimatedProperty?: (
|
|
element: DomEditSelection,
|
|
property: string,
|
|
value: number,
|
|
) => Promise<void>;
|
|
onRemoveKeyframe?: (animId: string, pct: number) => void;
|
|
onConvertToKeyframes?: (animId: string) => void;
|
|
}
|
|
|
|
function KeyframeGutter({
|
|
element,
|
|
property,
|
|
displayValue,
|
|
gsapAnimId,
|
|
navKeyframes,
|
|
currentPct,
|
|
seekFromKfPct,
|
|
animIdForProp,
|
|
onCommitAnimatedProperty,
|
|
onRemoveKeyframe,
|
|
onConvertToKeyframes,
|
|
}: {
|
|
property: string;
|
|
displayValue: number;
|
|
} & Pick<
|
|
GeometryRowsProps,
|
|
| "element"
|
|
| "gsapAnimId"
|
|
| "navKeyframes"
|
|
| "currentPct"
|
|
| "seekFromKfPct"
|
|
| "animIdForProp"
|
|
| "onCommitAnimatedProperty"
|
|
| "onRemoveKeyframe"
|
|
| "onConvertToKeyframes"
|
|
>) {
|
|
const track = useTrackDesignInput();
|
|
if (!gsapAnimId) return null;
|
|
const hasKeyframesOnProp = Boolean(navKeyframes?.some((kf) => property in kf.properties));
|
|
return (
|
|
<span data-flat-kf-gutter="true" style={{ opacity: hasKeyframesOnProp ? 1 : 0.3 }}>
|
|
<KeyframeNavigation
|
|
property={property}
|
|
keyframes={navKeyframes}
|
|
currentPercentage={currentPct}
|
|
onSeek={seekFromKfPct}
|
|
onAddKeyframe={() => {
|
|
if (!onCommitAnimatedProperty) return;
|
|
track("button", `Add ${property} keyframe`);
|
|
void onCommitAnimatedProperty(element, property, displayValue);
|
|
}}
|
|
onRemoveKeyframe={(pct, animationId) => {
|
|
if (!onRemoveKeyframe) return;
|
|
track("button", `Remove ${property} keyframe`);
|
|
onRemoveKeyframe(animationId ?? animIdForProp(property), pct);
|
|
}}
|
|
onConvertToKeyframes={() => {
|
|
if (!onConvertToKeyframes) return;
|
|
track("button", `Convert ${property} to keyframes`);
|
|
onConvertToKeyframes(animIdForProp(property));
|
|
}}
|
|
/>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function LayoutGeometryRows({
|
|
element,
|
|
displayX,
|
|
displayY,
|
|
displayW,
|
|
displayH,
|
|
displayR,
|
|
manualOffsetEditingDisabled,
|
|
manualSizeEditingDisabled,
|
|
manualRotationEditingDisabled,
|
|
commitManualOffset,
|
|
commitManualSize,
|
|
commitManualRotation,
|
|
gsapAnimId,
|
|
navKeyframes,
|
|
currentPct,
|
|
seekFromKfPct,
|
|
animIdForProp,
|
|
onCommitAnimatedProperty,
|
|
onRemoveKeyframe,
|
|
onConvertToKeyframes,
|
|
}: GeometryRowsProps) {
|
|
const gutterProps = {
|
|
element,
|
|
gsapAnimId,
|
|
navKeyframes,
|
|
currentPct,
|
|
seekFromKfPct,
|
|
animIdForProp,
|
|
onCommitAnimatedProperty,
|
|
onRemoveKeyframe,
|
|
onConvertToKeyframes,
|
|
};
|
|
return (
|
|
<>
|
|
<FlatRow
|
|
label="X"
|
|
value={formatPxMetricValue(displayX)}
|
|
tier={displayX === 0 ? "default" : "explicitCustom"}
|
|
disabled={manualOffsetEditingDisabled}
|
|
onCommit={(next) => commitManualOffset("x", next)}
|
|
suffix={<KeyframeGutter property="x" displayValue={displayX} {...gutterProps} />}
|
|
/>
|
|
<FlatRow
|
|
label="Y"
|
|
value={formatPxMetricValue(displayY)}
|
|
tier={displayY === 0 ? "default" : "explicitCustom"}
|
|
disabled={manualOffsetEditingDisabled}
|
|
onCommit={(next) => commitManualOffset("y", next)}
|
|
suffix={<KeyframeGutter property="y" displayValue={displayY} {...gutterProps} />}
|
|
/>
|
|
<FlatRow
|
|
label="W"
|
|
value={formatPxMetricValue(displayW)}
|
|
tier="default"
|
|
disabled={manualSizeEditingDisabled}
|
|
onCommit={(next) => commitManualSize("width", next)}
|
|
suffix={<KeyframeGutter property="width" displayValue={displayW} {...gutterProps} />}
|
|
/>
|
|
<FlatRow
|
|
label="H"
|
|
value={formatPxMetricValue(displayH)}
|
|
tier="default"
|
|
disabled={manualSizeEditingDisabled}
|
|
onCommit={(next) => commitManualSize("height", next)}
|
|
suffix={<KeyframeGutter property="height" displayValue={displayH} {...gutterProps} />}
|
|
/>
|
|
<FlatRow
|
|
label="Angle"
|
|
value={`${displayR}°`}
|
|
tier="default"
|
|
disabled={manualRotationEditingDisabled}
|
|
onCommit={(next) => commitManualRotation(next.replace("°", ""))}
|
|
suffix={<KeyframeGutter property="rotation" displayValue={displayR} {...gutterProps} />}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function LayoutZIndexRow({
|
|
styles,
|
|
onSetStyle,
|
|
}: {
|
|
styles: Record<string, string>;
|
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
}) {
|
|
const zIndex = String(parseInt(styles["z-index"] || "auto", 10) || 0);
|
|
return (
|
|
<FlatRow
|
|
label="Z-index"
|
|
value={zIndex}
|
|
tier="default"
|
|
onCommit={(next) => void onSetStyle("z-index", next)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function LayoutFlexBlock({
|
|
styles,
|
|
onSetStyle,
|
|
disabled,
|
|
}: {
|
|
styles: Record<string, string>;
|
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
disabled: boolean;
|
|
}) {
|
|
const isFlex = styles.display === "flex" || styles.display === "inline-flex";
|
|
if (!isFlex) return null;
|
|
const direction = styles["flex-direction"] || "row";
|
|
return (
|
|
<div className="border-l-2 border-panel-border-input py-0.5 pl-[10px]">
|
|
<div className="mb-[3px] text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
Flex
|
|
</div>
|
|
<FlatSegmentedRow
|
|
label="Direction"
|
|
options={[
|
|
{ key: "row", node: "→ Row", label: "Row", active: direction === "row" },
|
|
{ key: "column", node: "Column", label: "Column", active: direction === "column" },
|
|
]}
|
|
disabled={disabled}
|
|
onChange={(next) => void onSetStyle("flex-direction", next)}
|
|
/>
|
|
<FlatSelectRow
|
|
label="Justify"
|
|
value={styles["justify-content"] || "flex-start"}
|
|
tier={resolveValueTier(styles["justify-content"], "flex-start")}
|
|
disabled={disabled}
|
|
options={[
|
|
"flex-start",
|
|
"center",
|
|
"space-between",
|
|
"space-around",
|
|
"space-evenly",
|
|
"flex-end",
|
|
]}
|
|
onChange={(next) => void onSetStyle("justify-content", next)}
|
|
/>
|
|
<FlatSelectRow
|
|
label="Align"
|
|
value={styles["align-items"] || "stretch"}
|
|
tier={resolveValueTier(styles["align-items"], "stretch")}
|
|
disabled={disabled}
|
|
options={["stretch", "flex-start", "center", "flex-end", "baseline"]}
|
|
onChange={(next) => void onSetStyle("align-items", next)}
|
|
/>
|
|
<FlatRow
|
|
label="Gap"
|
|
value={styles.gap ?? "0px"}
|
|
tier={resolveValueTier(styles.gap, "0px")}
|
|
disabled={disabled}
|
|
onCommit={(next) => void onSetStyle("gap", next.endsWith("px") ? next : `${next}px`)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function LayoutTransform3DBlock({
|
|
gsapRuntimeValues,
|
|
gsapAnimId,
|
|
resolveAnimIdForProp,
|
|
gsapKeyframes,
|
|
currentPct,
|
|
elStart,
|
|
elDuration,
|
|
element,
|
|
onCommitAnimatedProperty,
|
|
onCommitAnimatedProperties,
|
|
onSeekToTime,
|
|
onRemoveKeyframe,
|
|
onConvertToKeyframes,
|
|
onLivePreviewProps,
|
|
}: {
|
|
gsapRuntimeValues: Record<string, number>;
|
|
gsapAnimId: string | null;
|
|
resolveAnimIdForProp?: (prop: string) => string | null;
|
|
gsapKeyframes: Array<{
|
|
percentage: number;
|
|
properties: Record<string, number | string>;
|
|
ease?: string;
|
|
}> | null;
|
|
currentPct: number;
|
|
elStart: number;
|
|
elDuration: number;
|
|
element: DomEditSelection;
|
|
onCommitAnimatedProperty?: (
|
|
element: DomEditSelection,
|
|
property: string,
|
|
value: number,
|
|
) => Promise<void>;
|
|
onCommitAnimatedProperties?: (
|
|
element: DomEditSelection,
|
|
props: Record<string, number | string>,
|
|
) => Promise<void>;
|
|
onSeekToTime?: (time: number) => void;
|
|
onRemoveKeyframe?: (animId: string, pct: number) => void;
|
|
onConvertToKeyframes?: (animId: string, duration?: number) => void;
|
|
onLivePreviewProps?: (element: DomEditSelection, props: Record<string, number>) => void;
|
|
}) {
|
|
return (
|
|
<div className="border-t border-panel-hairline pt-2.5">
|
|
<div className="mb-[3px] text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
3D Transform
|
|
</div>
|
|
<PropertyPanel3dTransform
|
|
gsapRuntimeValues={gsapRuntimeValues}
|
|
gsapAnimId={gsapAnimId}
|
|
resolveAnimIdForProp={resolveAnimIdForProp}
|
|
gsapKeyframes={gsapKeyframes}
|
|
currentPct={currentPct}
|
|
elStart={elStart}
|
|
elDuration={elDuration}
|
|
element={element}
|
|
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
onSeekToTime={onSeekToTime}
|
|
onRemoveKeyframe={onRemoveKeyframe}
|
|
onConvertToKeyframes={onConvertToKeyframes}
|
|
onLivePreviewProps={onLivePreviewProps}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FlatLayoutSectionProps
|
|
extends
|
|
Omit<GeometryRowsProps, never>,
|
|
Pick<
|
|
Parameters<typeof LayoutTransform3DBlock>[0],
|
|
| "gsapRuntimeValues"
|
|
| "resolveAnimIdForProp"
|
|
| "gsapKeyframes"
|
|
| "elStart"
|
|
| "elDuration"
|
|
| "onCommitAnimatedProperties"
|
|
| "onSeekToTime"
|
|
| "onLivePreviewProps"
|
|
> {
|
|
element: DomEditSelection;
|
|
styles: Record<string, string>;
|
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
disabled: boolean;
|
|
}
|
|
|
|
export function FlatLayoutSection({
|
|
element,
|
|
styles,
|
|
onSetStyle,
|
|
disabled,
|
|
gsapRuntimeValues,
|
|
resolveAnimIdForProp,
|
|
gsapKeyframes,
|
|
elStart,
|
|
elDuration,
|
|
onCommitAnimatedProperties,
|
|
onSeekToTime,
|
|
onLivePreviewProps,
|
|
...geometry
|
|
}: FlatLayoutSectionProps) {
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<LayoutGeometryRows element={element} {...geometry} />
|
|
<LayoutZIndexRow styles={styles} onSetStyle={onSetStyle} />
|
|
<LayoutFlexBlock styles={styles} onSetStyle={onSetStyle} disabled={disabled} />
|
|
<LayoutTransform3DBlock
|
|
gsapRuntimeValues={gsapRuntimeValues}
|
|
gsapAnimId={geometry.gsapAnimId}
|
|
resolveAnimIdForProp={resolveAnimIdForProp}
|
|
gsapKeyframes={gsapKeyframes}
|
|
currentPct={geometry.currentPct}
|
|
elStart={elStart}
|
|
elDuration={elDuration}
|
|
element={element}
|
|
onCommitAnimatedProperty={geometry.onCommitAnimatedProperty}
|
|
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
onSeekToTime={onSeekToTime}
|
|
onRemoveKeyframe={geometry.onRemoveKeyframe}
|
|
onConvertToKeyframes={geometry.onConvertToKeyframes}
|
|
onLivePreviewProps={onLivePreviewProps}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|