mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): wire the flat Motion group into the one-open accordion
This commit is contained in:
@@ -130,9 +130,23 @@ function flexElement() {
|
||||
};
|
||||
}
|
||||
|
||||
// Motion fixture (Plan 3b Task 4): an authored clip range (data-start present)
|
||||
// makes resolveEditingSections turn on `sections.timing`, so the Motion group
|
||||
// renders via its Timing gate even with no GSAP edit handlers wired.
|
||||
function animatedElement() {
|
||||
return {
|
||||
...baseElement(),
|
||||
id: "anim-clip",
|
||||
selector: ".anim-clip",
|
||||
label: "Anim Clip",
|
||||
dataAttributes: { start: "0", duration: "4" },
|
||||
};
|
||||
}
|
||||
|
||||
async function renderPanel(
|
||||
flatEnabled: boolean,
|
||||
elementOverride: ReturnType<typeof baseElement> = baseElement(),
|
||||
propsOverride: Partial<PropertyPanelProps> = {},
|
||||
) {
|
||||
vi.resetModules();
|
||||
vi.doMock("./manualEditingAvailability", async () => {
|
||||
@@ -154,6 +168,7 @@ async function renderPanel(
|
||||
onSetStyle: vi.fn(),
|
||||
onSetText: vi.fn(),
|
||||
onSetAttributeLive: vi.fn(),
|
||||
...propsOverride,
|
||||
} as unknown as PropertyPanelProps;
|
||||
act(() => {
|
||||
root.render(<PropertyPanel {...props} />);
|
||||
@@ -166,6 +181,18 @@ async function renderPanel(
|
||||
// default under heavy parallel full-suite load, so give these a wider margin.
|
||||
const RENDER_TIMEOUT_MS = 20_000;
|
||||
|
||||
// Find the collapsed accordion row whose title matches and click it open.
|
||||
function openFlatGroup(host: HTMLElement, title: string) {
|
||||
const row = Array.from(host.querySelectorAll('[data-flat-group-collapsed="true"]')).find((el) =>
|
||||
el.textContent?.includes(title),
|
||||
);
|
||||
if (!row) throw new Error(`expected a collapsed ${title} row`);
|
||||
act(() => row.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||
}
|
||||
|
||||
const openGroupText = (host: HTMLElement) =>
|
||||
host.querySelector('[data-flat-group-open="true"]')?.textContent ?? "";
|
||||
|
||||
describe("PropertyPanel — STUDIO_FLAT_INSPECTOR_ENABLED off", () => {
|
||||
it(
|
||||
"renders the legacy header, not the flat header",
|
||||
@@ -329,3 +356,65 @@ describe("PropertyPanel — Layout group (Plan 3a)", () => {
|
||||
RENDER_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
describe("PropertyPanel — Motion group (Plan 3b)", () => {
|
||||
it(
|
||||
"renders the Motion group with Timing, and opening it closes the previously open group (4-way exclusivity)",
|
||||
async () => {
|
||||
const { host, root } = await renderPanel(true, animatedElement());
|
||||
// Text is open by default for the text-editable fixture.
|
||||
expect(openGroupText(host)).toContain("Text");
|
||||
|
||||
openFlatGroup(host, "Motion");
|
||||
const openGroup = openGroupText(host);
|
||||
expect(openGroup).toContain("Motion");
|
||||
// FlatTimingRow (Start/End/Duration) renders inside the Motion group.
|
||||
expect(openGroup).toContain("Start");
|
||||
expect(openGroup).toContain("Duration");
|
||||
// One-open accordion: opening Motion closed the Text group.
|
||||
expect(openGroup).not.toContain("Text");
|
||||
|
||||
// Reverse direction: opening Layout closes Motion.
|
||||
openFlatGroup(host, "Layout");
|
||||
const openAfter = openGroupText(host);
|
||||
expect(openAfter).toContain("Layout");
|
||||
expect(openAfter).not.toContain("Motion");
|
||||
act(() => root.unmount());
|
||||
},
|
||||
RENDER_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it(
|
||||
"hides the effect list (showEffects off) when the GSAP edit handlers are absent",
|
||||
async () => {
|
||||
// STUDIO_GSAP_PANEL_ENABLED defaults on, but none of the five required
|
||||
// edit handlers are supplied here, so the effect-list half of the
|
||||
// double-gate stays closed — only the Timing row shows.
|
||||
const { host, root } = await renderPanel(true, animatedElement());
|
||||
openFlatGroup(host, "Motion");
|
||||
const openGroup = openGroupText(host);
|
||||
expect(openGroup).toContain("Motion");
|
||||
expect(openGroup).toContain("Duration"); // Timing still shows
|
||||
expect(openGroup).not.toContain("Add effect"); // effects gated off
|
||||
act(() => root.unmount());
|
||||
},
|
||||
RENDER_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it(
|
||||
"shows the effect list (showEffects on) when the flag and all five handlers are present",
|
||||
async () => {
|
||||
const { host, root } = await renderPanel(true, animatedElement(), {
|
||||
onUpdateGsapProperty: vi.fn(),
|
||||
onUpdateGsapMeta: vi.fn(),
|
||||
onDeleteGsapAnimation: vi.fn(),
|
||||
onAddGsapProperty: vi.fn(),
|
||||
onAddGsapAnimation: vi.fn(),
|
||||
});
|
||||
openFlatGroup(host, "Motion");
|
||||
expect(openGroupText(host)).toContain("Add effect");
|
||||
act(() => root.unmount());
|
||||
},
|
||||
RENDER_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,14 +10,29 @@ import { FlatGroup } from "./propertyPanelFlatPrimitives";
|
||||
import { FlatTextSection } from "./propertyPanelFlatTextSection";
|
||||
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
|
||||
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
|
||||
import { FlatMotionSection } from "./propertyPanelFlatMotionSection";
|
||||
import { createGsapLivePreview } from "./gsapLivePreview";
|
||||
import { formatTextFieldPreview, StyleSections } from "./propertyPanelSections";
|
||||
import { TimingSection } from "./propertyPanelTimingSection";
|
||||
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
|
||||
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
||||
import { MediaSection } from "./propertyPanelMediaSection";
|
||||
|
||||
type EditingSections = ReturnType<typeof resolveEditingSections>;
|
||||
|
||||
// Type-only fallback for the Motion effect-card callbacks. Used solely to
|
||||
// satisfy FlatMotionSection's required-callback shape when the effect list is
|
||||
// gated off (showEffects === false, so none of these are ever invoked). Keeps
|
||||
// the gated-off path free of `!` non-null assertions — the real, narrowed
|
||||
// handlers flow through only when the double-gate below passes.
|
||||
const EMPTY_GSAP_EFFECT_HANDLERS = {
|
||||
onAddAnimation: () => {},
|
||||
onUpdateProperty: () => {},
|
||||
onUpdateMeta: () => {},
|
||||
onDeleteAnimation: () => {},
|
||||
onAddProperty: () => {},
|
||||
onRemoveProperty: () => {},
|
||||
};
|
||||
|
||||
/**
|
||||
* The flat "Ledger" inspector shell (design_handoff_studio_inspector).
|
||||
*
|
||||
@@ -25,10 +40,8 @@ type EditingSections = ReturnType<typeof resolveEditingSections>;
|
||||
* (same one-directional-import precedent as FlatTextSection). Rendered only
|
||||
* when STUDIO_FLAT_INSPECTOR_ENABLED is on; owns the one-open/pin group state.
|
||||
*
|
||||
* Intentionally omits the Layout `Section` and `GsapAnimationSection` (Motion)
|
||||
* — flattening those is Layout/Motion plan territory (plans 3–4). A text
|
||||
* element with the flag on will not show Layout/Motion controls; that
|
||||
* regression is scoped and acceptable for an unreleased, flag-gated feature.
|
||||
* The Text/Style/Layout/Motion groups share the one-open accordion. The legacy
|
||||
* Media and Color-Grading sections render unchanged below the flat groups.
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
export function PropertyPanelFlat({
|
||||
@@ -90,6 +103,22 @@ export function PropertyPanelFlat({
|
||||
onSeekToTime,
|
||||
onRemoveKeyframe,
|
||||
onConvertToKeyframes,
|
||||
gsapMultipleTimelines,
|
||||
gsapUnsupportedTimelinePattern,
|
||||
onUpdateGsapProperty,
|
||||
onUpdateGsapMeta,
|
||||
onDeleteGsapAnimation,
|
||||
onAddGsapProperty,
|
||||
onRemoveGsapProperty,
|
||||
onUpdateGsapFromProperty,
|
||||
onAddGsapFromProperty,
|
||||
onRemoveGsapFromProperty,
|
||||
onAddGsapAnimation,
|
||||
onSetArcPath,
|
||||
onUpdateArcSegment,
|
||||
onUnroll,
|
||||
onUpdateKeyframeEase,
|
||||
onSetAllKeyframeEases,
|
||||
}: Pick<
|
||||
PropertyPanelProps,
|
||||
| "projectId"
|
||||
@@ -114,6 +143,22 @@ export function PropertyPanelFlat({
|
||||
| "onImportFonts"
|
||||
| "fontAssets"
|
||||
| "gsapAnimations"
|
||||
| "gsapMultipleTimelines"
|
||||
| "gsapUnsupportedTimelinePattern"
|
||||
| "onUpdateGsapProperty"
|
||||
| "onUpdateGsapMeta"
|
||||
| "onDeleteGsapAnimation"
|
||||
| "onAddGsapProperty"
|
||||
| "onRemoveGsapProperty"
|
||||
| "onUpdateGsapFromProperty"
|
||||
| "onAddGsapFromProperty"
|
||||
| "onRemoveGsapFromProperty"
|
||||
| "onAddGsapAnimation"
|
||||
| "onSetArcPath"
|
||||
| "onUpdateArcSegment"
|
||||
| "onUnroll"
|
||||
| "onUpdateKeyframeEase"
|
||||
| "onSetAllKeyframeEases"
|
||||
| "recordingState"
|
||||
| "recordingDuration"
|
||||
| "onToggleRecording"
|
||||
@@ -181,6 +226,43 @@ export function PropertyPanelFlat({
|
||||
// PropertyPanel (keeps that file under its 600-LOC gate).
|
||||
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
||||
|
||||
// Motion group double-gate — reproduces the legacy PropertyPanel gate exactly:
|
||||
// • Timing (sections.timing) shows via resolveEditingSections, same as today.
|
||||
// • The effect-card list shows only when STUDIO_GSAP_PANEL_ENABLED is on AND
|
||||
// all five edit handlers are present (identical to PropertyPanel's legacy
|
||||
// `<GsapAnimationSection>` guard).
|
||||
// Computing the narrowed handler bundle inside the `&&`-guarded ternary lets
|
||||
// TypeScript prove each handler non-undefined without a `!` assertion; the
|
||||
// noop bundle only fills the type when the gate is off (never invoked, since
|
||||
// FlatMotionSection guards every call behind showEffects).
|
||||
const showMotionTiming = Boolean(sections.timing);
|
||||
const gsapEffectHandlers =
|
||||
STUDIO_GSAP_PANEL_ENABLED &&
|
||||
onUpdateGsapProperty &&
|
||||
onUpdateGsapMeta &&
|
||||
onDeleteGsapAnimation &&
|
||||
onAddGsapProperty &&
|
||||
onAddGsapAnimation
|
||||
? {
|
||||
onAddAnimation: onAddGsapAnimation,
|
||||
onUpdateProperty: onUpdateGsapProperty,
|
||||
onUpdateMeta: onUpdateGsapMeta,
|
||||
onDeleteAnimation: onDeleteGsapAnimation,
|
||||
onAddProperty: onAddGsapProperty,
|
||||
onRemoveProperty: onRemoveGsapProperty ?? (() => {}),
|
||||
onUpdateFromProperty: onUpdateGsapFromProperty,
|
||||
onAddFromProperty: onAddGsapFromProperty,
|
||||
onRemoveFromProperty: onRemoveGsapFromProperty,
|
||||
onSetArcPath,
|
||||
onUpdateArcSegment,
|
||||
onUnroll,
|
||||
onUpdateKeyframeEase,
|
||||
onSetAllKeyframeEases,
|
||||
}
|
||||
: null;
|
||||
const showMotionEffects = gsapEffectHandlers !== null;
|
||||
const showMotionGroup = showMotionTiming || showMotionEffects;
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
|
||||
<PropertyPanelFlatHeader
|
||||
@@ -287,12 +369,26 @@ export function PropertyPanelFlat({
|
||||
/>
|
||||
</FlatGroup>
|
||||
|
||||
{sections.timing && (
|
||||
<TimingSection
|
||||
element={element}
|
||||
animations={gsapAnimations}
|
||||
onSetAttribute={onSetAttribute}
|
||||
/>
|
||||
{showMotionGroup && (
|
||||
<FlatGroup
|
||||
title="Motion"
|
||||
isOpen={openGroupId === "motion" || pinnedGroupIds.includes("motion")}
|
||||
isPinned={pinnedGroupIds.includes("motion")}
|
||||
onToggleOpen={() => toggleOpen("motion")}
|
||||
onTogglePin={() => togglePin("motion")}
|
||||
summary={`${gsapAnimations.length} effect${gsapAnimations.length === 1 ? "" : "s"}`}
|
||||
>
|
||||
<FlatMotionSection
|
||||
element={element}
|
||||
animations={gsapAnimations}
|
||||
showTiming={showMotionTiming}
|
||||
showEffects={showMotionEffects}
|
||||
multipleTimelines={gsapMultipleTimelines}
|
||||
unsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
|
||||
onSetAttribute={onSetAttribute}
|
||||
{...(gsapEffectHandlers ?? EMPTY_GSAP_EFFECT_HANDLERS)}
|
||||
/>
|
||||
</FlatGroup>
|
||||
)}
|
||||
{sections.colorGrading && (
|
||||
<ColorGradingSection
|
||||
|
||||
Reference in New Issue
Block a user