diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index f497696dba..5960d782d8 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -272,6 +272,16 @@ "file": "packages/studio/src/components/editor/propertyPanelSections.tsx", "exports": ["TextAreaField"], }, + // Link: its only consumer was FlatRadiusRow's uniform-only fallback row in + // propertyPanelFlatStyleSections.tsx, deleted by the Style parity fix + // (p8-task-style-parity) — that row was unreachable from a uniform radius + // and is now replaced by BorderRadiusEditor's own unlink toggle. Kept in + // the icon set for future reuse rather than deleted from a file outside + // this fix's scope. + { + "file": "packages/studio/src/icons/SystemIcons.tsx", + "exports": ["Link"], + }, ], "ignoreDependencies": [ // Runtime/dynamic deps not visible to static analysis: tsup `external`, diff --git a/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx index a1ee58b829..73ef2a06fb 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx @@ -103,6 +103,110 @@ describe("FlatColorGradingAccessory", () => { expect(resetGrading).toHaveBeenCalledTimes(1); act(() => root.unmount()); }); + + it("shows the runtime status message as visible text next to the dot, not only as a title", () => { + const { host, root } = renderInto( + , + ); + const messageEl = host.querySelector('[data-flat-grade-status-message="true"]'); + expect(messageEl).not.toBeNull(); + expect(messageEl?.textContent).toBe("Waiting for shader"); + expect(host.textContent).toContain("Waiting for shader"); + act(() => root.unmount()); + }); + + function activeGrading() { + const grading = neutralGrading(); + return { ...grading, adjust: { ...grading.adjust, contrast: 0.2 } }; + } + + it("activates hold-to-compare on pointerdown and releases on window pointerup", () => { + const commitCompare = vi.fn(); + const { host, root } = renderInto( + , + ); + const compareButton = host.querySelector( + '[aria-label="Hold to show original"]', + ); + if (!compareButton) throw new Error("expected a compare button"); + act(() => compareButton.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }))); + expect(commitCompare).toHaveBeenNthCalledWith(1, true); + act(() => window.dispatchEvent(new MouseEvent("pointerup", { bubbles: true }))); + expect(commitCompare).toHaveBeenNthCalledWith(2, false); + act(() => root.unmount()); + }); + + it("activates hold-to-compare via keyboard Space and releases on keyup", () => { + const commitCompare = vi.fn(); + const { host, root } = renderInto( + , + ); + const compareButton = host.querySelector( + '[aria-label="Hold to show original"]', + ); + if (!compareButton) throw new Error("expected a compare button"); + act(() => + compareButton.dispatchEvent( + new KeyboardEvent("keydown", { key: " ", bubbles: true, cancelable: true }), + ), + ); + expect(commitCompare).toHaveBeenNthCalledWith(1, true); + act(() => + compareButton.dispatchEvent( + new KeyboardEvent("keyup", { key: " ", bubbles: true, cancelable: true }), + ), + ); + expect(commitCompare).toHaveBeenNthCalledWith(2, false); + act(() => root.unmount()); + }); + + it("releases an active hold when the window loses focus mid-hold", () => { + const commitCompare = vi.fn(); + const { host, root } = renderInto( + , + ); + const compareButton = host.querySelector( + '[aria-label="Hold to show original"]', + ); + if (!compareButton) throw new Error("expected a compare button"); + act(() => compareButton.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }))); + expect(commitCompare).toHaveBeenNthCalledWith(1, true); + act(() => window.dispatchEvent(new Event("blur"))); + expect(commitCompare).toHaveBeenNthCalledWith(2, false); + act(() => root.unmount()); + }); }); function neutralPropsBase() { @@ -280,6 +384,43 @@ describe("FlatColorGradingSection — Adjust sliders", () => { expect(onCommitColorGrading.mock.calls[0][0].adjust.saturation).toBe(0.2); act(() => root.unmount()); }); + + it("revives a grade parked at 0% strength back to 100% when an Adjust slider is committed", () => { + const onCommitColorGrading = vi.fn(); + const grading = { ...neutralGrading(), intensity: 0 }; + const { host, root } = renderInto( + , + ); + const contrastRow = findRowByText(host, '[data-flat-grade-adjust="true"]', "Contrast"); + // min=-100, max=100, step=1, ratio=0.75 -> raw=50 -> commit(50) -> adjust.contrast = 0.5 + dragSliderTrack(contrastRow, 75, 100); + expect(onCommitColorGrading).toHaveBeenCalledTimes(1); + expect(onCommitColorGrading.mock.calls[0][0].intensity).toBe(1); + expect(onCommitColorGrading.mock.calls[0][0].adjust.contrast).toBe(0.5); + act(() => root.unmount()); + }); + + it("does NOT force intensity to revive when the Strength slider itself is dragged — it writes the value directly", () => { + const onCommitColorGrading = vi.fn(); + const grading = { ...neutralGrading(), intensity: 0 }; + const { host, root } = renderInto( + , + ); + const strengthRow = findRowByText(host, "div", "Strength", "startsWith"); + // min=0, max=100, step=1, ratio=0.4 -> raw=40 -> commit(40) -> intensity = 40/100 = 0.4 + dragSliderTrack(strengthRow, 40, 100); + expect(onCommitColorGrading).toHaveBeenCalledTimes(1); + expect(onCommitColorGrading.mock.calls[0][0].intensity).toBe(0.4); + act(() => root.unmount()); + }); }); describe("FlatColorGradingSection — Vignette and Grain", () => { @@ -386,6 +527,46 @@ describe("FlatColorGradingSection — HDR banner and Apply scope", () => { act(() => root.unmount()); }); + it("shows a codec/profile/pixel-format/color detail line in the HDR banner when metadata provides it", () => { + const { host, root } = renderInto( + , + ); + const detail = host.querySelector('[data-flat-grade-hdr-detail="true"]'); + expect(detail).not.toBeNull(); + expect(detail?.textContent).toBe("hevc · Main10 · yuv420p10le · bt2020 · smpte2084"); + act(() => root.unmount()); + }); + + it("omits the HDR detail line entirely when no detail fields are populated", () => { + const { host, root } = renderInto( + , + ); + expect(host.querySelector('[data-flat-grade-hdr-detail="true"]')).toBeNull(); + act(() => root.unmount()); + }); + it("omits the HDR banner for SDR media", () => { const { host, root } = renderInto( { + if (compareEnabled) commitCompare(false); + }} + onKeyDown={(e) => { + if (!gradingActive || (e.key !== " " && e.key !== "Enter")) return; + e.preventDefault(); + if (!compareEnabled) commitCompare(true); + }} + onKeyUp={(e) => { + if (!gradingActive || (e.key !== " " && e.key !== "Enter")) return; + e.preventDefault(); + commitCompare(false); }} 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" > - + + + + {runtimeStatus.message} + +