feat(studio): add HDR warning banner and Apply-to-scope row, completing FlatColorGradingSection

Also adds a not-busy click assertion for the Apply button so
onApplyToScope is exercised end-to-end, not just its disabled state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-14 15:51:52 -07:00
co-authored by Claude Sonnet 5
parent 80dc7c49f6
commit cc07c84a31
2 changed files with 110 additions and 6 deletions
@@ -370,3 +370,59 @@ describe("FlatColorGradingSection — Effects", () => {
act(() => root.unmount());
});
});
describe("FlatColorGradingSection — HDR banner and Apply scope", () => {
it("shows the HDR banner only when mediaMetadata reports an HDR source", () => {
const { host, root } = renderInto(
<FlatColorGradingSection
{...neutralPropsBase()}
mediaMetadata={{
kind: "video",
color: { dynamicRange: "hdr", hdrTransfer: "pq", label: "HDR10", isHdr: true },
}}
/>,
);
expect(host.textContent).toContain("SDR preview");
act(() => root.unmount());
});
it("omits the HDR banner for SDR media", () => {
const { host, root } = renderInto(
<FlatColorGradingSection
{...neutralPropsBase()}
mediaMetadata={{
kind: "video",
color: { dynamicRange: "sdr", hdrTransfer: null, label: "SDR", isHdr: false },
}}
/>,
);
expect(host.textContent).not.toContain("SDR preview");
act(() => root.unmount());
});
it("fires onApplyToScope from the Apply button, respecting applyBusy", () => {
const onApplyToScope = vi.fn();
const { host, root } = renderInto(
<FlatColorGradingSection {...neutralPropsBase()} onApplyToScope={onApplyToScope} applyBusy />,
);
const applyButton = host.querySelector<HTMLButtonElement>('[data-flat-grade-apply="true"]');
expect(applyButton?.disabled).toBe(true);
act(() => root.unmount());
});
it("fires onApplyToScope exactly once when the Apply button is clicked while not busy", () => {
const onApplyToScope = vi.fn();
const { host, root } = renderInto(
<FlatColorGradingSection
{...neutralPropsBase()}
onApplyToScope={onApplyToScope}
applyBusy={false}
/>,
);
const applyButton = host.querySelector<HTMLButtonElement>('[data-flat-grade-apply="true"]');
expect(applyButton?.disabled).toBe(false);
act(() => applyButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onApplyToScope).toHaveBeenCalledTimes(1);
act(() => root.unmount());
});
});
@@ -138,18 +138,39 @@ const EFFECT_SLIDERS: Array<{ key: HfColorGradingEffectKey; label: string }> = [
{ key: "pixelate", label: "Pixelate" },
];
function HdrBanner({ metadata }: { metadata: MediaMetadata | null }) {
if (metadata?.color.dynamicRange !== "hdr") return null;
return (
<div
data-flat-grade-hdr-banner="true"
className="rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-1.5 text-[10px] leading-4 text-amber-100"
>
<div className="mb-0.5 flex items-center justify-between gap-2">
<span className="font-semibold">{metadata.color.label} source</span>
<span className="rounded bg-amber-400/20 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-amber-100">
SDR preview
</span>
</div>
<p className="text-amber-100/80">
These controls use the current SDR shader preview path. Render may stay HDR-tagged, but this
is not true HDR color grading yet.
</p>
</div>
);
}
// fallow-ignore-next-line complexity
export function FlatColorGradingSection({
grading,
assets,
onImportAssets,
onCommitColorGrading,
applyScope: _applyScope,
applyBusy: _applyBusy,
onSetApplyScope: _onSetApplyScope,
onApplyToScope: _onApplyToScope,
onApplyScopeAvailable: _onApplyScopeAvailable,
mediaMetadata: _mediaMetadata,
applyScope,
applyBusy,
onSetApplyScope,
onApplyToScope,
onApplyScopeAvailable,
mediaMetadata,
}: {
grading: NormalizedHfColorGrading;
assets: string[];
@@ -218,6 +239,7 @@ export function FlatColorGradingSection({
return (
<div className="space-y-1.5">
<HdrBanner metadata={mediaMetadata} />
<div data-flat-grade-preset="true" className="flex min-h-[30px] items-center justify-between">
<span className="text-[11px] text-panel-text-2">Preset</span>
<FlatSelectRow
@@ -424,6 +446,32 @@ export function FlatColorGradingSection({
);
})}
</div>
{onApplyScopeAvailable && (
<div className="flex items-center justify-between gap-2 border-t border-panel-hairline pt-1.5">
<span className="flex items-center gap-1.5 text-[11px] text-panel-text-2">
Copy grade to
<select
value={applyScope}
onChange={(e) => onSetApplyScope(e.target.value as "source-file" | "project")}
disabled={applyBusy}
className="bg-transparent font-mono text-[11px] text-panel-text-0 outline-none disabled:opacity-50"
>
<option value="source-file">Current file media</option>
<option value="project">All project media</option>
</select>
</span>
<button
type="button"
data-flat-grade-apply="true"
disabled={applyBusy}
onClick={onApplyToScope}
className="text-[11px] font-medium text-panel-accent hover:text-panel-accent/80 disabled:cursor-not-allowed disabled:opacity-50"
>
{applyBusy ? "Applying" : "Apply"}
</button>
</div>
)}
</div>
);
}