refactor(studio): split oversized files to pass 600-line check (#1313)

This commit is contained in:
Miguel Ángel
2026-06-09 19:53:14 -04:00
committed by GitHub
parent d13ae13670
commit 81416ab3c9
17 changed files with 1380 additions and 1055 deletions
@@ -7,11 +7,14 @@ import {
formatPxMetricValue,
parsePxMetricValue,
RESPONSIVE_GRID,
readGsapRuntimeValuesForPanel,
readGsapBorderRadiusForPanel,
} from "./propertyPanelHelpers";
import { MetricField, Section } from "./propertyPanelPrimitives";
import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
import { TextSection, StyleSections } from "./propertyPanelSections";
import { GsapAnimationSection } from "./GsapAnimationSection";
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
import { KeyframeNavigation } from "./KeyframeNavigation";
import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
import { usePlayerStore } from "../../player";
@@ -188,69 +191,19 @@ export const PropertyPanel = memo(function PropertyPanel({
gsapAnimations?.find((a) => a.keyframes)?.id ?? gsapAnimations?.[0]?.id ?? null;
// Read ALL GSAP-interpolated values at the current seek time.
// Discovers animated properties from the animation's keyframes/tween vars.
const gsapRuntimeValues: Record<string, number> | null = (() => {
if (!gsapAnimId || gsapAnimations.length === 0) return null;
const iframe = previewIframeRef?.current;
if (!iframe?.contentWindow) return null;
const selector = element.id ? `#${element.id}` : element.selector;
if (!selector) return null;
try {
const gsap = (
iframe.contentWindow as unknown as {
gsap?: { getProperty: (el: Element, prop: string) => number | string };
}
).gsap;
if (!gsap?.getProperty) return null;
const el = iframe.contentDocument?.querySelector(selector);
if (!el) return null;
const propKeys = new Set<string>();
for (const anim of gsapAnimations) {
if (anim.keyframes) {
for (const kf of anim.keyframes.keyframes) {
for (const p of Object.keys(kf.properties)) propKeys.add(p);
}
}
for (const p of Object.keys(anim.properties)) propKeys.add(p);
}
const result: Record<string, number> = {};
for (const prop of propKeys) {
const v = Number(gsap.getProperty(el, prop));
if (Number.isFinite(v)) result[prop] = Math.round(v * 100) / 100;
}
return Object.keys(result).length > 0 ? result : null;
} catch {
return null;
}
})();
const gsapRuntimeValues = readGsapRuntimeValuesForPanel(
gsapAnimId,
gsapAnimations,
element,
previewIframeRef ?? { current: null },
);
const gsapBorderRadius: { tl: number; tr: number; br: number; bl: number } | null = (() => {
if (!gsapRuntimeValues || !("borderRadius" in gsapRuntimeValues)) {
const hasBRProp = gsapAnimations.some(
(a) =>
"borderRadius" in a.properties ||
a.keyframes?.keyframes.some((kf) => "borderRadius" in kf.properties),
);
if (!hasBRProp) return null;
}
const iframe = previewIframeRef?.current;
const selector = element.id ? `#${element.id}` : element.selector;
if (!iframe?.contentDocument || !selector) return null;
try {
const el = iframe.contentDocument.querySelector(selector);
if (!el) return null;
const cs = iframe.contentWindow!.getComputedStyle(el);
const parse = (v: string) => Number.parseFloat(v) || 0;
return {
tl: parse(cs.borderTopLeftRadius),
tr: parse(cs.borderTopRightRadius),
br: parse(cs.borderBottomRightRadius),
bl: parse(cs.borderBottomLeftRadius),
};
} catch {
return null;
}
})();
const gsapBorderRadius = readGsapBorderRadiusForPanel(
gsapRuntimeValues,
gsapAnimations,
element,
previewIframeRef ?? { current: null },
);
const displayX = gsapRuntimeValues?.x ?? manualOffset.x;
const displayY = gsapRuntimeValues?.y ?? manualOffset.y;
@@ -510,97 +463,19 @@ export const PropertyPanel = memo(function PropertyPanel({
</div>
</div>
{gsapRuntimeValues && (
<div className="mt-3 border-t border-neutral-800/40 pt-3">
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
3D Transform
</div>
<div className={RESPONSIVE_GRID}>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="Z"
value={formatPxMetricValue(gsapRuntimeValues.z ?? 0)}
scrub
onCommit={(next) => {
const v = parsePxMetricValue(next);
if (v != null && onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "z", v);
}
}}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
<KeyframeNavigation
property="z"
keyframes={gsapKeyframes}
currentPercentage={currentPct}
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
onAddKeyframe={() => {
if (onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "z", gsapRuntimeValues?.z ?? 0);
}
}}
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
/>
)}
</div>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="Scale"
value={String(gsapRuntimeValues.scale ?? 1)}
scrub
onCommit={(next) => {
const v = Number.parseFloat(next);
if (Number.isFinite(v) && onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "scale", v);
}
}}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
<KeyframeNavigation
property="scale"
keyframes={gsapKeyframes}
currentPercentage={currentPct}
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
onAddKeyframe={() => {
if (onCommitAnimatedProperty) {
void onCommitAnimatedProperty(
element,
"scale",
gsapRuntimeValues?.scale ?? 1,
);
}
}}
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
/>
)}
</div>
<MetricField
label="RotX"
value={`${gsapRuntimeValues.rotationX ?? 0}°`}
onCommit={(next) => {
const v = Number.parseFloat(next.replace("°", ""));
if (Number.isFinite(v) && onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "rotationX", v);
}
}}
/>
<MetricField
label="RotY"
value={`${gsapRuntimeValues.rotationY ?? 0}°`}
onCommit={(next) => {
const v = Number.parseFloat(next.replace("°", ""));
if (Number.isFinite(v) && onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "rotationY", v);
}
}}
/>
</div>
</div>
<PropertyPanel3dTransform
gsapRuntimeValues={gsapRuntimeValues}
gsapAnimId={gsapAnimId}
gsapKeyframes={gsapKeyframes}
currentPct={currentPct}
elStart={elStart}
elDuration={elDuration}
element={element}
onCommitAnimatedProperty={onCommitAnimatedProperty}
onSeekToTime={onSeekToTime}
onRemoveKeyframe={onRemoveKeyframe}
onConvertToKeyframes={onConvertToKeyframes}
/>
)}
<div className="mt-3">
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
@@ -0,0 +1,52 @@
/**
* Checks whether GSAP actively animates one or more CSS/GSAP properties on
* the given element by inspecting all registered `__timelines`.
*/
export function gsapAnimatesProperty(el: HTMLElement, ...props: string[]): boolean {
const win = el.ownerDocument.defaultView as
| (Window & {
__timelines?: Record<
string,
{
getChildren?: (
deep: boolean,
) => Array<{ targets?: () => Element[]; vars?: Record<string, unknown> }>;
}
>;
})
| null;
if (!win?.__timelines) return false;
const propSet = new Set(props);
for (const tl of Object.values(win.__timelines)) {
if (!tl?.getChildren) continue;
try {
for (const child of tl.getChildren(true)) {
if (!child.targets || !child.vars) continue;
let targetsEl = false;
for (const t of child.targets()) {
if (t === el || (el.id && t.id === el.id)) {
targetsEl = true;
break;
}
}
if (!targetsEl) continue;
const vars = child.vars;
for (const p of propSet) {
if (p in vars) return true;
}
if (vars.keyframes && typeof vars.keyframes === "object") {
for (const kfVal of Object.values(vars.keyframes as Record<string, unknown>)) {
if (kfVal && typeof kfVal === "object") {
for (const p of propSet) {
if (p in (kfVal as Record<string, unknown>)) return true;
}
}
}
}
}
} catch {
/* */
}
}
return false;
}
@@ -32,6 +32,7 @@ import {
} from "./manualEditsTypes";
import { roundRotationAngle } from "./manualEditsParsing";
import { applyStudioMotionFromDom } from "./studioMotion";
import { gsapAnimatesProperty } from "./gsapAnimatesProperty";
/* ── Gesture tracking ─────────────────────────────────────────────── */
let studioManualEditGestureId = 0;
@@ -534,55 +535,6 @@ function reapplyPathOffsets(doc: Document): void {
}
}
function gsapAnimatesProperty(el: HTMLElement, ...props: string[]): boolean {
const win = el.ownerDocument.defaultView as
| (Window & {
__timelines?: Record<
string,
{
getChildren?: (
deep: boolean,
) => Array<{ targets?: () => Element[]; vars?: Record<string, unknown> }>;
}
>;
})
| null;
if (!win?.__timelines) return false;
const propSet = new Set(props);
for (const tl of Object.values(win.__timelines)) {
if (!tl?.getChildren) continue;
try {
for (const child of tl.getChildren(true)) {
if (!child.targets || !child.vars) continue;
let targetsEl = false;
for (const t of child.targets()) {
if (t === el || (el.id && t.id === el.id)) {
targetsEl = true;
break;
}
}
if (!targetsEl) continue;
const vars = child.vars;
for (const p of propSet) {
if (p in vars) return true;
}
if (vars.keyframes && typeof vars.keyframes === "object") {
for (const kfVal of Object.values(vars.keyframes as Record<string, unknown>)) {
if (kfVal && typeof kfVal === "object") {
for (const p of propSet) {
if (p in (kfVal as Record<string, unknown>)) return true;
}
}
}
}
}
} catch {
/* */
}
}
return false;
}
function reapplyBoxSizes(doc: Document): void {
for (const el of queryStudioElements(doc, STUDIO_BOX_SIZE_ATTR)) {
if (gsapAnimatesProperty(el, "width", "height")) continue;
@@ -0,0 +1,133 @@
import type { DomEditSelection } from "./domEditingTypes";
import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
import { MetricField } from "./propertyPanelPrimitives";
import { KeyframeNavigation } from "./KeyframeNavigation";
import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
type KeyframeEntry = Array<{
percentage: number;
properties: Record<string, number | string>;
ease?: string;
}> | null;
interface PropertyPanel3dTransformProps {
gsapRuntimeValues: Record<string, number>;
gsapAnimId: string | null;
gsapKeyframes: KeyframeEntry;
currentPct: number;
elStart: number;
elDuration: number;
element: DomEditSelection;
onCommitAnimatedProperty?: (
element: DomEditSelection,
property: string,
value: number,
) => Promise<void>;
onSeekToTime?: (time: number) => void;
onRemoveKeyframe?: (animId: string, pct: number) => void;
onConvertToKeyframes?: (animId: string) => void;
}
export function PropertyPanel3dTransform({
gsapRuntimeValues,
gsapAnimId,
gsapKeyframes,
currentPct,
elStart,
elDuration,
element,
onCommitAnimatedProperty,
onSeekToTime,
onRemoveKeyframe,
onConvertToKeyframes,
}: PropertyPanel3dTransformProps) {
return (
<div className="mt-3 border-t border-neutral-800/40 pt-3">
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
3D Transform
</div>
<div className={RESPONSIVE_GRID}>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="Z"
value={formatPxMetricValue(gsapRuntimeValues.z ?? 0)}
scrub
onCommit={(next) => {
const v = parsePxMetricValue(next);
if (v != null && onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "z", v);
}
}}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
<KeyframeNavigation
property="z"
keyframes={gsapKeyframes}
currentPercentage={currentPct}
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
onAddKeyframe={() => {
if (onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "z", gsapRuntimeValues?.z ?? 0);
}
}}
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
/>
)}
</div>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="Scale"
value={String(gsapRuntimeValues.scale ?? 1)}
scrub
onCommit={(next) => {
const v = Number.parseFloat(next);
if (Number.isFinite(v) && onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "scale", v);
}
}}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
<KeyframeNavigation
property="scale"
keyframes={gsapKeyframes}
currentPercentage={currentPct}
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
onAddKeyframe={() => {
if (onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "scale", gsapRuntimeValues?.scale ?? 1);
}
}}
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
/>
)}
</div>
<MetricField
label="RotX"
value={`${gsapRuntimeValues.rotationX ?? 0}°`}
onCommit={(next) => {
const v = Number.parseFloat(next.replace("°", ""));
if (Number.isFinite(v) && onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "rotationX", v);
}
}}
/>
<MetricField
label="RotY"
value={`${gsapRuntimeValues.rotationY ?? 0}°`}
onCommit={(next) => {
const v = Number.parseFloat(next.replace("°", ""));
if (Number.isFinite(v) && onCommitAnimatedProperty) {
void onCommitAnimatedProperty(element, "rotationY", v);
}
}}
/>
</div>
</div>
);
}
@@ -2,6 +2,7 @@ import { parseCssColor, type ParsedColor } from "./colorValue";
import { COMMON_LOCAL_FONT_FAMILIES } from "./fontCatalog";
import type { DomEditSelection } from "./domEditing";
import type { ImportedFontAsset } from "./fontAssets";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
export interface PropertyPanelProps {
projectId: string;
@@ -505,3 +506,78 @@ export function computeFitToChildrenSize(
const height = Math.round((maxY - minY) * scaleY);
return width > 0 && height > 0 ? { width, height } : null;
}
// ── GSAP runtime value readers (used by PropertyPanel) ────────────────────
export function readGsapRuntimeValuesForPanel(
gsapAnimId: string | null,
gsapAnimations: GsapAnimation[],
element: DomEditSelection,
previewIframeRef: React.RefObject<HTMLIFrameElement | null>,
): Record<string, number> | null {
if (!gsapAnimId || gsapAnimations.length === 0) return null;
const iframe = previewIframeRef?.current;
if (!iframe?.contentWindow) return null;
const selector = element.id ? `#${element.id}` : element.selector;
if (!selector) return null;
try {
const gsap = (
iframe.contentWindow as unknown as {
gsap?: { getProperty: (el: Element, prop: string) => number | string };
}
).gsap;
if (!gsap?.getProperty) return null;
const el = iframe.contentDocument?.querySelector(selector);
if (!el) return null;
const propKeys = new Set<string>();
for (const anim of gsapAnimations) {
if (anim.keyframes) {
for (const kf of anim.keyframes.keyframes) {
for (const p of Object.keys(kf.properties)) propKeys.add(p);
}
}
for (const p of Object.keys(anim.properties)) propKeys.add(p);
}
const result: Record<string, number> = {};
for (const prop of propKeys) {
const v = Number(gsap.getProperty(el, prop));
if (Number.isFinite(v)) result[prop] = Math.round(v * 100) / 100;
}
return Object.keys(result).length > 0 ? result : null;
} catch {
return null;
}
}
export function readGsapBorderRadiusForPanel(
gsapRuntimeValues: Record<string, number> | null,
gsapAnimations: GsapAnimation[],
element: DomEditSelection,
previewIframeRef: React.RefObject<HTMLIFrameElement | null>,
): { tl: number; tr: number; br: number; bl: number } | null {
if (!gsapRuntimeValues || !("borderRadius" in gsapRuntimeValues)) {
const hasBRProp = gsapAnimations.some(
(a) =>
"borderRadius" in a.properties ||
a.keyframes?.keyframes.some((kf) => "borderRadius" in kf.properties),
);
if (!hasBRProp) return null;
}
const iframe = previewIframeRef?.current;
const selector = element.id ? `#${element.id}` : element.selector;
if (!iframe?.contentDocument || !selector) return null;
try {
const el = iframe.contentDocument.querySelector(selector);
if (!el) return null;
const cs = iframe.contentWindow!.getComputedStyle(el);
const parse = (v: string) => Number.parseFloat(v) || 0;
return {
tl: parse(cs.borderTopLeftRadius),
tr: parse(cs.borderTopRightRadius),
br: parse(cs.borderBottomRightRadius),
bl: parse(cs.borderBottomLeftRadius),
};
} catch {
return null;
}
}