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(
|
async function renderPanel(
|
||||||
flatEnabled: boolean,
|
flatEnabled: boolean,
|
||||||
elementOverride: ReturnType<typeof baseElement> = baseElement(),
|
elementOverride: ReturnType<typeof baseElement> = baseElement(),
|
||||||
|
propsOverride: Partial<PropertyPanelProps> = {},
|
||||||
) {
|
) {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
vi.doMock("./manualEditingAvailability", async () => {
|
vi.doMock("./manualEditingAvailability", async () => {
|
||||||
@@ -154,6 +168,7 @@ async function renderPanel(
|
|||||||
onSetStyle: vi.fn(),
|
onSetStyle: vi.fn(),
|
||||||
onSetText: vi.fn(),
|
onSetText: vi.fn(),
|
||||||
onSetAttributeLive: vi.fn(),
|
onSetAttributeLive: vi.fn(),
|
||||||
|
...propsOverride,
|
||||||
} as unknown as PropertyPanelProps;
|
} as unknown as PropertyPanelProps;
|
||||||
act(() => {
|
act(() => {
|
||||||
root.render(<PropertyPanel {...props} />);
|
root.render(<PropertyPanel {...props} />);
|
||||||
@@ -166,6 +181,18 @@ async function renderPanel(
|
|||||||
// default under heavy parallel full-suite load, so give these a wider margin.
|
// default under heavy parallel full-suite load, so give these a wider margin.
|
||||||
const RENDER_TIMEOUT_MS = 20_000;
|
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", () => {
|
describe("PropertyPanel — STUDIO_FLAT_INSPECTOR_ENABLED off", () => {
|
||||||
it(
|
it(
|
||||||
"renders the legacy header, not the flat header",
|
"renders the legacy header, not the flat header",
|
||||||
@@ -329,3 +356,65 @@ describe("PropertyPanel — Layout group (Plan 3a)", () => {
|
|||||||
RENDER_TIMEOUT_MS,
|
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 { FlatTextSection } from "./propertyPanelFlatTextSection";
|
||||||
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
|
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
|
||||||
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
|
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
|
||||||
|
import { FlatMotionSection } from "./propertyPanelFlatMotionSection";
|
||||||
import { createGsapLivePreview } from "./gsapLivePreview";
|
import { createGsapLivePreview } from "./gsapLivePreview";
|
||||||
import { formatTextFieldPreview, StyleSections } from "./propertyPanelSections";
|
import { formatTextFieldPreview, StyleSections } from "./propertyPanelSections";
|
||||||
import { TimingSection } from "./propertyPanelTimingSection";
|
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
|
||||||
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
||||||
import { MediaSection } from "./propertyPanelMediaSection";
|
import { MediaSection } from "./propertyPanelMediaSection";
|
||||||
|
|
||||||
type EditingSections = ReturnType<typeof resolveEditingSections>;
|
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).
|
* 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
|
* (same one-directional-import precedent as FlatTextSection). Rendered only
|
||||||
* when STUDIO_FLAT_INSPECTOR_ENABLED is on; owns the one-open/pin group state.
|
* when STUDIO_FLAT_INSPECTOR_ENABLED is on; owns the one-open/pin group state.
|
||||||
*
|
*
|
||||||
* Intentionally omits the Layout `Section` and `GsapAnimationSection` (Motion)
|
* The Text/Style/Layout/Motion groups share the one-open accordion. The legacy
|
||||||
* — flattening those is Layout/Motion plan territory (plans 3–4). A text
|
* Media and Color-Grading sections render unchanged below the flat groups.
|
||||||
* element with the flag on will not show Layout/Motion controls; that
|
|
||||||
* regression is scoped and acceptable for an unreleased, flag-gated feature.
|
|
||||||
*/
|
*/
|
||||||
// fallow-ignore-next-line complexity
|
// fallow-ignore-next-line complexity
|
||||||
export function PropertyPanelFlat({
|
export function PropertyPanelFlat({
|
||||||
@@ -90,6 +103,22 @@ export function PropertyPanelFlat({
|
|||||||
onSeekToTime,
|
onSeekToTime,
|
||||||
onRemoveKeyframe,
|
onRemoveKeyframe,
|
||||||
onConvertToKeyframes,
|
onConvertToKeyframes,
|
||||||
|
gsapMultipleTimelines,
|
||||||
|
gsapUnsupportedTimelinePattern,
|
||||||
|
onUpdateGsapProperty,
|
||||||
|
onUpdateGsapMeta,
|
||||||
|
onDeleteGsapAnimation,
|
||||||
|
onAddGsapProperty,
|
||||||
|
onRemoveGsapProperty,
|
||||||
|
onUpdateGsapFromProperty,
|
||||||
|
onAddGsapFromProperty,
|
||||||
|
onRemoveGsapFromProperty,
|
||||||
|
onAddGsapAnimation,
|
||||||
|
onSetArcPath,
|
||||||
|
onUpdateArcSegment,
|
||||||
|
onUnroll,
|
||||||
|
onUpdateKeyframeEase,
|
||||||
|
onSetAllKeyframeEases,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
PropertyPanelProps,
|
PropertyPanelProps,
|
||||||
| "projectId"
|
| "projectId"
|
||||||
@@ -114,6 +143,22 @@ export function PropertyPanelFlat({
|
|||||||
| "onImportFonts"
|
| "onImportFonts"
|
||||||
| "fontAssets"
|
| "fontAssets"
|
||||||
| "gsapAnimations"
|
| "gsapAnimations"
|
||||||
|
| "gsapMultipleTimelines"
|
||||||
|
| "gsapUnsupportedTimelinePattern"
|
||||||
|
| "onUpdateGsapProperty"
|
||||||
|
| "onUpdateGsapMeta"
|
||||||
|
| "onDeleteGsapAnimation"
|
||||||
|
| "onAddGsapProperty"
|
||||||
|
| "onRemoveGsapProperty"
|
||||||
|
| "onUpdateGsapFromProperty"
|
||||||
|
| "onAddGsapFromProperty"
|
||||||
|
| "onRemoveGsapFromProperty"
|
||||||
|
| "onAddGsapAnimation"
|
||||||
|
| "onSetArcPath"
|
||||||
|
| "onUpdateArcSegment"
|
||||||
|
| "onUnroll"
|
||||||
|
| "onUpdateKeyframeEase"
|
||||||
|
| "onSetAllKeyframeEases"
|
||||||
| "recordingState"
|
| "recordingState"
|
||||||
| "recordingDuration"
|
| "recordingDuration"
|
||||||
| "onToggleRecording"
|
| "onToggleRecording"
|
||||||
@@ -181,6 +226,43 @@ export function PropertyPanelFlat({
|
|||||||
// PropertyPanel (keeps that file under its 600-LOC gate).
|
// PropertyPanel (keeps that file under its 600-LOC gate).
|
||||||
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
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 (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
|
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
|
||||||
<PropertyPanelFlatHeader
|
<PropertyPanelFlatHeader
|
||||||
@@ -287,12 +369,26 @@ export function PropertyPanelFlat({
|
|||||||
/>
|
/>
|
||||||
</FlatGroup>
|
</FlatGroup>
|
||||||
|
|
||||||
{sections.timing && (
|
{showMotionGroup && (
|
||||||
<TimingSection
|
<FlatGroup
|
||||||
element={element}
|
title="Motion"
|
||||||
animations={gsapAnimations}
|
isOpen={openGroupId === "motion" || pinnedGroupIds.includes("motion")}
|
||||||
onSetAttribute={onSetAttribute}
|
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 && (
|
{sections.colorGrading && (
|
||||||
<ColorGradingSection
|
<ColorGradingSection
|
||||||
|
|||||||
Reference in New Issue
Block a user