From 6c253042d7d227df6a299bb9282cb0fb8c2c24e4 Mon Sep 17 00:00:00 2001 From: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com> Date: Sun, 14 Jun 2026 02:13:18 -0300 Subject: [PATCH 1/2] fix(studio): route rotation field edits through the animation like X/Y/W/H MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the PropertyPanel transform section, typing into X/Y or W/H on a GSAP-animated element routes the value into the animation (via `onCommitAnimatedProperty`, or a new keyframe). `commitManualRotation` did not — it always went straight to `onSetManualRotation`, so typing a rotation on an animated element behaved differently from every other transform field, even though rotation is a supported animated property and both the rotate-drag gesture and the rotation keyframe button already route through the animation. Extract the shared animated-transform routing (animated property -> keyframe -> "no callbacks" toast) into `commitAnimatedTransformValue`, and have both `commitManualOffset` and `commitManualRotation` use it. Non-animated elements still fall through to the manual setter unchanged. `commitManualSize` keeps its own (keyframe-less) shape. --- .../src/components/editor/PropertyPanel.tsx | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/studio/src/components/editor/PropertyPanel.tsx b/packages/studio/src/components/editor/PropertyPanel.tsx index bf77e7c71..b2c62d231 100644 --- a/packages/studio/src/components/editor/PropertyPanel.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.tsx @@ -158,22 +158,41 @@ export const PropertyPanel = memo(function PropertyPanel({ ? manualSize.height : (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height); - const commitManualOffset = (axis: "x" | "y", nextValue: string) => { - const parsed = parsePxMetricValue(nextValue); - if (parsed == null) return; + // Route a transform value into the GSAP animation (or a new keyframe) when the + // element is animated. Returns true when handled, so callers fall through to + // the manual-transform path only for non-animated elements. + const commitAnimatedTransformValue = ( + property: string, + value: number, + noCallbacksMessage: string, + ): boolean => { if (onCommitAnimatedProperty && hasGsapAnimation) { - void onCommitAnimatedProperty(element, axis, parsed); - return; + void onCommitAnimatedProperty(element, property, value); + return true; } if (gsapKeyframes && gsapAnimId && onAddKeyframe) { const pct = Math.max(0, Math.min(100, Math.round(currentPct * 10) / 10)); - onAddKeyframe(gsapAnimId, pct, axis, parsed); - return; + onAddKeyframe(gsapAnimId, pct, property, value); + return true; } if (hasGsapAnimation) { - showToast?.("Cannot edit position — animation callbacks not available"); - return; + showToast?.(noCallbacksMessage); + return true; } + return false; + }; + + const commitManualOffset = (axis: "x" | "y", nextValue: string) => { + const parsed = parsePxMetricValue(nextValue); + if (parsed == null) return; + if ( + commitAnimatedTransformValue( + axis, + parsed, + "Cannot edit position — animation callbacks not available", + ) + ) + return; const current = readStudioPathOffset(element.element); void Promise.resolve( onSetManualOffset(element, { @@ -216,6 +235,14 @@ export const PropertyPanel = memo(function PropertyPanel({ const commitManualRotation = (nextValue: string) => { const parsed = Number.parseFloat(nextValue); if (!Number.isFinite(parsed)) return; + if ( + commitAnimatedTransformValue( + "rotation", + parsed, + "Cannot edit rotation — animation callbacks not available", + ) + ) + return; void Promise.resolve(onSetManualRotation(element, { angle: parsed })).catch(() => undefined); }; From 48b14fbf954b87dcefca09673c79858ce370ddde Mon Sep 17 00:00:00 2001 From: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com> Date: Sun, 14 Jun 2026 02:49:10 -0300 Subject: [PATCH 2/2] refactor(studio): move transform commit handlers to a co-located util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review on #1427: the inline `commitAnimatedTransformValue` extraction pushed PropertyPanel.tsx to 613 lines (over the 600 limit). Move the transform commit factory (`commitAnimatedTransformValue` + the X/Y, W/H and rotation handlers) into `propertyPanelTransformCommit.ts` and call it from the panel. PropertyPanel.tsx drops to 543 lines; behavior is unchanged. Co-authored-by: Miguel Ángel --- .../src/components/editor/PropertyPanel.tsx | 102 +++----------- .../editor/propertyPanelTransformCommit.ts | 129 ++++++++++++++++++ 2 files changed, 145 insertions(+), 86 deletions(-) create mode 100644 packages/studio/src/components/editor/propertyPanelTransformCommit.ts diff --git a/packages/studio/src/components/editor/PropertyPanel.tsx b/packages/studio/src/components/editor/PropertyPanel.tsx index b2c62d231..07d7da520 100644 --- a/packages/studio/src/components/editor/PropertyPanel.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.tsx @@ -11,6 +11,7 @@ import { readGsapBorderRadiusForPanel, } from "./propertyPanelHelpers"; import { MetricField, Section } from "./propertyPanelPrimitives"; +import { createTransformCommitHandlers } from "./propertyPanelTransformCommit"; import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser"; import { isMediaElement, MediaSection } from "./propertyPanelMediaSection"; import { TextSection, StyleSections } from "./propertyPanelSections"; @@ -158,93 +159,7 @@ export const PropertyPanel = memo(function PropertyPanel({ ? manualSize.height : (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height); - // Route a transform value into the GSAP animation (or a new keyframe) when the - // element is animated. Returns true when handled, so callers fall through to - // the manual-transform path only for non-animated elements. - const commitAnimatedTransformValue = ( - property: string, - value: number, - noCallbacksMessage: string, - ): boolean => { - if (onCommitAnimatedProperty && hasGsapAnimation) { - void onCommitAnimatedProperty(element, property, value); - return true; - } - if (gsapKeyframes && gsapAnimId && onAddKeyframe) { - const pct = Math.max(0, Math.min(100, Math.round(currentPct * 10) / 10)); - onAddKeyframe(gsapAnimId, pct, property, value); - return true; - } - if (hasGsapAnimation) { - showToast?.(noCallbacksMessage); - return true; - } - return false; - }; - - const commitManualOffset = (axis: "x" | "y", nextValue: string) => { - const parsed = parsePxMetricValue(nextValue); - if (parsed == null) return; - if ( - commitAnimatedTransformValue( - axis, - parsed, - "Cannot edit position — animation callbacks not available", - ) - ) - return; - const current = readStudioPathOffset(element.element); - void Promise.resolve( - onSetManualOffset(element, { - x: axis === "x" ? parsed : current.x, - y: axis === "y" ? parsed : current.y, - }), - ).catch(() => undefined); - }; - - // fallow-ignore-next-line complexity - const commitManualSize = (axis: "width" | "height", nextValue: string) => { - const parsed = parsePxMetricValue(nextValue); - if (parsed == null || parsed <= 0) return; - if (onCommitAnimatedProperty && hasGsapAnimation) { - void onCommitAnimatedProperty(element, axis, parsed); - return; - } - if (hasGsapAnimation) { - showToast?.("Cannot edit size — animation callbacks not available"); - return; - } - const current = readStudioBoxSize(element.element); - const width = - current.width > 0 - ? current.width - : (parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width); - const height = - current.height > 0 - ? current.height - : (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height); - void Promise.resolve( - onSetManualSize(element, { - width: axis === "width" ? parsed : width, - height: axis === "height" ? parsed : height, - }), - ).catch(() => undefined); - }; - const manualRotation = readStudioRotation(element.element); - const commitManualRotation = (nextValue: string) => { - const parsed = Number.parseFloat(nextValue); - if (!Number.isFinite(parsed)) return; - if ( - commitAnimatedTransformValue( - "rotation", - parsed, - "Cannot edit rotation — animation callbacks not available", - ) - ) - return; - void Promise.resolve(onSetManualRotation(element, { angle: parsed })).catch(() => undefined); - }; const elStart = Number.parseFloat(element?.dataAttributes?.start ?? "0") || 0; const elDuration = Number.parseFloat(element?.dataAttributes?.duration ?? "1") || 0; @@ -254,6 +169,21 @@ export const PropertyPanel = memo(function PropertyPanel({ const gsapKeyframes = gsapKfAnim?.keyframes?.keyframes ?? null; const gsapAnimId = gsapKfAnim?.id ?? gsapAnimations?.[0]?.id ?? null; const hasGsapAnimation = !!(gsapAnimId || gsapAnimations.length > 0); + const { commitManualOffset, commitManualSize, commitManualRotation } = + createTransformCommitHandlers({ + element, + styles, + hasGsapAnimation, + gsapAnimId, + gsapKeyframes, + currentPct, + onCommitAnimatedProperty, + onAddKeyframe, + onSetManualOffset, + onSetManualSize, + onSetManualRotation, + showToast, + }); const navKeyframes = cacheEntry?.keyframes ?? gsapKeyframes; const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration); diff --git a/packages/studio/src/components/editor/propertyPanelTransformCommit.ts b/packages/studio/src/components/editor/propertyPanelTransformCommit.ts new file mode 100644 index 000000000..ec4a948ab --- /dev/null +++ b/packages/studio/src/components/editor/propertyPanelTransformCommit.ts @@ -0,0 +1,129 @@ +import type { DomEditSelection } from "./domEditingTypes"; +import { readStudioBoxSize, readStudioPathOffset } from "./manualEdits"; +import { parsePxMetricValue, type PropertyPanelProps } from "./propertyPanelHelpers"; + +interface TransformCommitDeps { + element: DomEditSelection; + styles: Record; + hasGsapAnimation: boolean; + gsapAnimId: string | null; + gsapKeyframes: unknown; + currentPct: number; + onCommitAnimatedProperty: PropertyPanelProps["onCommitAnimatedProperty"]; + onAddKeyframe: PropertyPanelProps["onAddKeyframe"]; + onSetManualOffset: PropertyPanelProps["onSetManualOffset"]; + onSetManualSize: PropertyPanelProps["onSetManualSize"]; + onSetManualRotation: PropertyPanelProps["onSetManualRotation"]; + showToast?: (message: string, tone?: "error" | "info") => void; +} + +/** + * Build the X/Y, W/H and rotation field commit handlers for the property panel. + * Each handler routes the value into the GSAP animation when the element is + * animated (matching the drag gesture and keyframe buttons), and otherwise + * falls through to the manual transform setter. + */ +// fallow-ignore-next-line unit-size +export function createTransformCommitHandlers({ + element, + styles, + hasGsapAnimation, + gsapAnimId, + gsapKeyframes, + currentPct, + onCommitAnimatedProperty, + onAddKeyframe, + onSetManualOffset, + onSetManualSize, + onSetManualRotation, + showToast, +}: TransformCommitDeps) { + // Route a transform value into the GSAP animation (or a new keyframe) when the + // element is animated. Returns true when handled, so callers fall through to + // the manual-transform path only for non-animated elements. + const commitAnimatedTransformValue = ( + property: string, + value: number, + noCallbacksMessage: string, + ): boolean => { + if (onCommitAnimatedProperty && hasGsapAnimation) { + void onCommitAnimatedProperty(element, property, value); + return true; + } + if (gsapKeyframes && gsapAnimId && onAddKeyframe) { + const pct = Math.max(0, Math.min(100, Math.round(currentPct * 10) / 10)); + onAddKeyframe(gsapAnimId, pct, property, value); + return true; + } + if (hasGsapAnimation) { + showToast?.(noCallbacksMessage); + return true; + } + return false; + }; + + const commitManualOffset = (axis: "x" | "y", nextValue: string) => { + const parsed = parsePxMetricValue(nextValue); + if (parsed == null) return; + if ( + commitAnimatedTransformValue( + axis, + parsed, + "Cannot edit position — animation callbacks not available", + ) + ) + return; + const current = readStudioPathOffset(element.element); + void Promise.resolve( + onSetManualOffset(element, { + x: axis === "x" ? parsed : current.x, + y: axis === "y" ? parsed : current.y, + }), + ).catch(() => undefined); + }; + + // fallow-ignore-next-line complexity + const commitManualSize = (axis: "width" | "height", nextValue: string) => { + const parsed = parsePxMetricValue(nextValue); + if (parsed == null || parsed <= 0) return; + if (onCommitAnimatedProperty && hasGsapAnimation) { + void onCommitAnimatedProperty(element, axis, parsed); + return; + } + if (hasGsapAnimation) { + showToast?.("Cannot edit size — animation callbacks not available"); + return; + } + const current = readStudioBoxSize(element.element); + const width = + current.width > 0 + ? current.width + : (parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width); + const height = + current.height > 0 + ? current.height + : (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height); + void Promise.resolve( + onSetManualSize(element, { + width: axis === "width" ? parsed : width, + height: axis === "height" ? parsed : height, + }), + ).catch(() => undefined); + }; + + const commitManualRotation = (nextValue: string) => { + const parsed = Number.parseFloat(nextValue); + if (!Number.isFinite(parsed)) return; + if ( + commitAnimatedTransformValue( + "rotation", + parsed, + "Cannot edit rotation — animation callbacks not available", + ) + ) + return; + void Promise.resolve(onSetManualRotation(element, { angle: parsed })).catch(() => undefined); + }; + + return { commitManualOffset, commitManualSize, commitManualRotation }; +}