From 0369865ddb1ff334fb79708f83d26535ab3dc8cd Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Thu, 9 Jul 2026 11:21:36 -0700 Subject: [PATCH] feat(studio): compose FlatLayoutSection from geometry, z-index, flex, and 3D blocks --- .../propertyPanelFlatLayoutSection.test.tsx | 102 ++++++++++++++++++ .../editor/propertyPanelFlatLayoutSection.tsx | 71 +++++++++++- 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.test.tsx index 6ecad450f..ece488c96 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.test.tsx @@ -4,6 +4,7 @@ import React, { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, describe, expect, it, vi } from "vitest"; import { + FlatLayoutSection, LayoutFlexBlock, LayoutGeometryRows, LayoutTransform3DBlock, @@ -36,6 +37,7 @@ function getFlatRowInput(host: HTMLElement, label: string): HTMLInputElement { function baseGeometryProps(overrides: Partial[0]> = {}) { return { + element: {} as never, displayX: 0, displayY: -24, displayW: 257.4, @@ -111,6 +113,32 @@ describe("LayoutGeometryRows", () => { expect(full.length).toBeGreaterThan(0); act(() => root.unmount()); }); + + it("passes the real element/selection (not null) to onCommitAnimatedProperty when adding a keyframe", () => { + const onCommitAnimatedProperty = vi.fn(); + const element = { id: "el-1" } as unknown as Parameters< + typeof LayoutGeometryRows + >[0]["element"]; + const { host, root } = renderInto( + , + ); + const addButton = host.querySelector('[title="Add x keyframe"]'); + if (!addButton) throw new Error("expected an Add x keyframe button"); + act(() => { + (addButton as HTMLElement).dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + expect(onCommitAnimatedProperty).toHaveBeenCalledWith(element, "x", 0); + expect(onCommitAnimatedProperty).not.toHaveBeenCalledWith(null, "x", 0); + act(() => root.unmount()); + }); }); describe("LayoutZIndexRow", () => { @@ -189,3 +217,77 @@ describe("LayoutTransform3DBlock", () => { act(() => root.unmount()); }); }); + +describe("FlatLayoutSection", () => { + it("renders geometry rows, z-index, flex (when applicable), and the 3D transform block in order", () => { + const { host, root } = renderInto( + p} + gsapRuntimeValues={{}} + gsapKeyframes={null} + elStart={0} + elDuration={0} + onSeekToTime={vi.fn()} + />, + ); + const text = host.textContent ?? ""; + expect(text).toContain("X"); + expect(text).toContain("Z-index"); + expect(text).toContain("Flex"); + expect(text).toContain("3D Transform"); + act(() => root.unmount()); + }); + + it("omits the Flex block for a non-flex element", () => { + const { host, root } = renderInto( + p} + gsapRuntimeValues={{}} + gsapKeyframes={null} + elStart={0} + elDuration={0} + onSeekToTime={vi.fn()} + />, + ); + expect(host.textContent).not.toContain("Flex"); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx index e8e58e77a..0c7b0a2e4 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatLayoutSection.tsx @@ -14,6 +14,7 @@ type KeyframeEntry = Array<{ }> | null; interface GeometryRowsProps { + element: DomEditSelection; displayX: number; displayY: number; displayW: number; @@ -31,15 +32,16 @@ interface GeometryRowsProps { seekFromKfPct: (pct: number) => void; animIdForProp: (prop: string) => string; onCommitAnimatedProperty?: ( - element: unknown, + element: DomEditSelection, property: string, value: number, - ) => void | Promise; + ) => Promise; onRemoveKeyframe?: (animId: string, pct: number) => void; onConvertToKeyframes?: (animId: string) => void; } function KeyframeGutter({ + element, property, displayValue, gsapAnimId, @@ -55,6 +57,7 @@ function KeyframeGutter({ displayValue: number; } & Pick< GeometryRowsProps, + | "element" | "gsapAnimId" | "navKeyframes" | "currentPct" @@ -74,7 +77,7 @@ function KeyframeGutter({ currentPercentage={currentPct} onSeek={seekFromKfPct} onAddKeyframe={() => - onCommitAnimatedProperty && void onCommitAnimatedProperty(null, property, displayValue) + onCommitAnimatedProperty && void onCommitAnimatedProperty(element, property, displayValue) } onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp(property), pct)} onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp(property))} @@ -84,6 +87,7 @@ function KeyframeGutter({ } export function LayoutGeometryRows({ + element, displayX, displayY, displayW, @@ -105,6 +109,7 @@ export function LayoutGeometryRows({ onConvertToKeyframes, }: GeometryRowsProps) { const gutterProps = { + element, gsapAnimId, navKeyframes, currentPct, @@ -304,3 +309,63 @@ export function LayoutTransform3DBlock({ ); } + +interface FlatLayoutSectionProps + extends + Omit, + Pick< + Parameters[0], + | "gsapRuntimeValues" + | "resolveAnimIdForProp" + | "gsapKeyframes" + | "elStart" + | "elDuration" + | "onCommitAnimatedProperties" + | "onSeekToTime" + | "onLivePreviewProps" + > { + element: DomEditSelection; + styles: Record; + onSetStyle: (prop: string, value: string) => void | Promise; + disabled: boolean; +} + +export function FlatLayoutSection({ + element, + styles, + onSetStyle, + disabled, + gsapRuntimeValues, + resolveAnimIdForProp, + gsapKeyframes, + elStart, + elDuration, + onCommitAnimatedProperties, + onSeekToTime, + onLivePreviewProps, + ...geometry +}: FlatLayoutSectionProps) { + return ( +
+ + + + +
+ ); +}