Files
hyperframes/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx
T
Miguel Ángel cef3b86c95 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.
2026-07-30 02:00:05 +02:00

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>
);
}