feat(studio): wire the flat Grade group into the one-open accordion

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-14 15:51:53 -07:00
co-authored by Claude Sonnet 5
parent cc07c84a31
commit 384fe5934a
2 changed files with 63 additions and 0 deletions
@@ -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",
@@ -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<typeof resolveEditingSections>;
@@ -225,6 +230,20 @@ export function PropertyPanelFlat({
);
const [pinnedGroupIds, setPinnedGroupIds] = useState<string[]>([]);
// 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({
/>
</FlatGroup>
)}
{sections.colorGrading && (
<FlatGroup
title="Grade"
isOpen={openGroupId === "grade" || pinnedGroupIds.includes("grade")}
isPinned={pinnedGroupIds.includes("grade")}
onToggleOpen={() => toggleOpen("grade")}
onTogglePin={() => togglePin("grade")}
accessory={<FlatColorGradingAccessory state={colorGradingController} />}
summary={`${colorGradingController.grading.preset ?? "neutral"} · ${Math.round(colorGradingController.grading.intensity * 100)}%`}
>
<FlatColorGradingSection
grading={colorGradingController.grading}
assets={assets}
onImportAssets={onImportAssets}
onCommitColorGrading={colorGradingController.commitColorGrading}
applyScope={colorGradingController.applyScope}
applyBusy={colorGradingController.applyBusy}
onSetApplyScope={colorGradingController.setApplyScope}
onApplyToScope={() => void colorGradingController.applyToScope()}
onApplyScopeAvailable={Boolean(onApplyColorGradingScope)}
mediaMetadata={colorGradingController.mediaMetadata}
/>
</FlatGroup>
)}
{sections.colorGrading && (
<ColorGradingSection
key={[