feat(studio): add FlatColorGradingAccessory (compare, status dot, reset)

This commit is contained in:
Vance Ingalls
2026-07-14 15:50:50 -07:00
parent 18e4aef7dc
commit 160193d833
2 changed files with 141 additions and 0 deletions
@@ -0,0 +1,73 @@
// @vitest-environment happy-dom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FlatColorGradingAccessory } from "./propertyPanelFlatColorGradingSection";
import { normalizeHfColorGrading } from "@hyperframes/core/color-grading";
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
afterEach(() => {
document.body.innerHTML = "";
});
function renderInto(node: React.ReactElement) {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(node);
});
return { host, root };
}
function neutralGrading() {
const grading = normalizeHfColorGrading("neutral");
if (!grading) throw new Error("expected a neutral grading");
return grading;
}
describe("FlatColorGradingAccessory", () => {
it("shows a 5px status dot colored by runtime status, with the message as its title", () => {
const { host, root } = renderInto(
<FlatColorGradingAccessory
state={{
grading: neutralGrading(),
compareEnabled: false,
runtimeStatus: { state: "active", message: "Shader active" },
commitCompare: vi.fn(),
resetGrading: vi.fn(),
}}
/>,
);
const dot = host.querySelector('[data-flat-grade-status-dot="true"]');
expect(dot).not.toBeNull();
expect(dot?.getAttribute("title")).toBe("Shader active");
expect(dot?.className).toContain("bg-emerald-400");
act(() => root.unmount());
});
it("disables the compare hold button when grading is inactive, and fires resetGrading on click", () => {
const resetGrading = vi.fn();
const { host, root } = renderInto(
<FlatColorGradingAccessory
state={{
grading: neutralGrading(),
compareEnabled: false,
runtimeStatus: { state: "inactive", message: "No grading applied" },
commitCompare: vi.fn(),
resetGrading,
}}
/>,
);
const compareButton = host.querySelector<HTMLButtonElement>(
'[aria-label="Hold to show original"]',
);
expect(compareButton?.disabled).toBe(true);
const resetButton = host.querySelector<HTMLButtonElement>('[data-flat-grade-reset="true"]');
act(() => resetButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(resetGrading).toHaveBeenCalledTimes(1);
act(() => root.unmount());
});
});
@@ -0,0 +1,68 @@
import { isHfColorGradingActive } from "@hyperframes/core/color-grading";
import { Compare, RotateCcw } from "../../icons/SystemIcons";
import type { ColorGradingControllerState } from "./useColorGradingController";
const STATUS_DOT_CLASS: Record<ColorGradingControllerState["runtimeStatus"]["state"], string> = {
active: "bg-emerald-400",
pending: "bg-amber-300",
unavailable: "bg-red-400",
missing: "bg-panel-text-5",
inactive: "bg-panel-text-5",
};
export function FlatColorGradingAccessory({
state,
}: {
state: Pick<
ColorGradingControllerState,
"grading" | "compareEnabled" | "runtimeStatus" | "commitCompare" | "resetGrading"
>;
}) {
const { grading, compareEnabled, runtimeStatus, commitCompare, resetGrading } = state;
const gradingActive = isHfColorGradingActive(grading);
return (
<span className="flex items-center gap-2.5">
<button
type="button"
aria-pressed={compareEnabled}
aria-label="Hold to show original"
disabled={!gradingActive}
onPointerDown={(e) => {
if (!gradingActive) return;
e.preventDefault();
e.stopPropagation();
commitCompare(true);
const release = () => {
commitCompare(false);
window.removeEventListener("pointerup", release);
window.removeEventListener("pointercancel", release);
};
window.addEventListener("pointerup", release);
window.addEventListener("pointercancel", release);
}}
title="Hold to show original"
className="flex-shrink-0 text-panel-text-3 hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
>
<Compare size={12} />
</button>
<span
data-flat-grade-status-dot="true"
title={runtimeStatus.message}
className={`h-[5px] w-[5px] flex-shrink-0 rounded-full ${STATUS_DOT_CLASS[runtimeStatus.state]}`}
/>
<button
type="button"
data-flat-grade-reset="true"
title="Reset color grading"
onClick={(e) => {
e.stopPropagation();
resetGrading();
}}
className="flex-shrink-0 text-panel-text-3 hover:text-panel-text-1"
>
<RotateCcw size={12} />
</button>
</span>
);
}