diff --git a/packages/studio/src/components/editor/PropertyPanel.test.tsx b/packages/studio/src/components/editor/PropertyPanel.test.tsx index a3d854c7b..12ee79368 100644 --- a/packages/studio/src/components/editor/PropertyPanel.test.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.test.tsx @@ -668,6 +668,26 @@ function flatGroupTitles(host: HTMLElement): string[] { return [...open, ...collapsed]; } +describe("PropertyPanel โ€” Grade group (flag on)", () => { + it( + "renders the Grade group with its accessory for a grade-editable (video) element", + async () => { + const { host, root } = await renderPanel(true, { + ...baseElement(), + tagName: "video", + textFields: [], + }); + const gradeCollapsedOrOpen = + host.querySelector('[data-flat-group-collapsed="true"]') || + host.querySelector('[data-flat-group-open="true"]'); + expect(host.textContent).toContain("Grade"); + expect(gradeCollapsedOrOpen).not.toBeNull(); + act(() => root.unmount()); + }, + RENDER_TIMEOUT_MS, + ); +}); + describe("PropertyPanel โ€” Media group (Plan 4)", () => { it( "renders the flat Media group and not the legacy MediaSection, for a video element", diff --git a/packages/studio/src/components/editor/PropertyPanelFlat.tsx b/packages/studio/src/components/editor/PropertyPanelFlat.tsx index cdb1c9b2a..5656f3329 100644 --- a/packages/studio/src/components/editor/PropertyPanelFlat.tsx +++ b/packages/studio/src/components/editor/PropertyPanelFlat.tsx @@ -17,6 +17,11 @@ import { createGsapLivePreview } from "./gsapLivePreview"; import { formatTextFieldPreview, StyleSections } from "./propertyPanelSections"; import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability"; import { ColorGradingSection } from "./propertyPanelColorGradingSection"; +import { useColorGradingController } from "./useColorGradingController"; +import { + FlatColorGradingAccessory, + FlatColorGradingSection, +} from "./propertyPanelFlatColorGradingSection"; type EditingSections = ReturnType; @@ -225,6 +230,20 @@ export function PropertyPanelFlat({ ); const [pinnedGroupIds, setPinnedGroupIds] = useState([]); + // Grade group state. Called unconditionally (React rules-of-hooks) even when + // sections.colorGrading is false โ€” unlike the legacy ColorGradingSection, + // which is only mounted when the section is active, PropertyPanelFlat is not + // remounted per-section so the hook must run every render. Shares one state + // object between the group's header accessory (compare/status/reset) and its + // body (the FlatColorGradingSection controls). + const colorGradingController = useColorGradingController({ + projectId, + element, + previewIframeRef, + onSetAttributeLive, + onApplyScope: onApplyColorGradingScope, + }); + const isTextEditable = isTextEditableSelection(element); const elementKind = sections.media ? "media" : element.textFields.length > 0 ? "text" : "other"; const toggleOpen = (groupId: string) => @@ -415,6 +434,30 @@ export function PropertyPanelFlat({ /> )} + {sections.colorGrading && ( + toggleOpen("grade")} + onTogglePin={() => togglePin("grade")} + accessory={} + summary={`${colorGradingController.grading.preset ?? "neutral"} ยท ${Math.round(colorGradingController.grading.intensity * 100)}%`} + > + void colorGradingController.applyToScope()} + onApplyScopeAvailable={Boolean(onApplyColorGradingScope)} + mediaMetadata={colorGradingController.mediaMetadata} + /> + + )} {sections.colorGrading && (