From 138d45faac695fdccf6dfeee5cf94d92b00670eb Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Thu, 9 Jul 2026 13:46:19 -0700 Subject: [PATCH] feat(studio): add cutout sub-block to FlatMediaSection --- .../propertyPanelFlatMediaSection.test.tsx | 49 +++++++++++++++ .../editor/propertyPanelFlatMediaSection.tsx | 60 +++++++++++++++++-- 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx index 8791748ac..b20222a67 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.test.tsx @@ -80,3 +80,52 @@ describe("FlatMediaSection — source row", () => { act(() => root.unmount()); }); }); + +describe("FlatMediaSection — cutout", () => { + it("shows the WebM label for video and fires background removal on click", async () => { + const onRemoveBackground = vi.fn().mockResolvedValue({ outputPath: "assets/intro-loop.webm" }); + const onSetHtmlAttribute = vi.fn(); + const onSetAttribute = vi.fn(); + const host = document.createElement("div"); + document.body.append(host); + const root = createRoot(host); + const element = makeVideoElement(); + act(() => { + root.render( + , + ); + }); + expect(host.textContent).toContain("transparent WebM"); + const removeBgButton = host.querySelector( + '[data-flat-media-remove-bg="true"]', + ); + expect(removeBgButton).not.toBeNull(); + await act(async () => { + removeBgButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + await Promise.resolve(); + }); + expect(onRemoveBackground).toHaveBeenCalled(); + act(() => root.unmount()); + }); + + it("toggles BG plate via FlatToggle", () => { + const { host, root } = renderSection(); + const plateToggle = host.querySelector( + '[data-flat-toggle="true"][aria-label="BG plate"]', + ); + expect(plateToggle).not.toBeNull(); + expect(plateToggle?.getAttribute("aria-checked")).toBe("false"); + act(() => plateToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(plateToggle?.getAttribute("aria-checked")).toBe("true"); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx index 5d249da27..4daf973ba 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatMediaSection.tsx @@ -6,7 +6,9 @@ import { type BackgroundRemovalResult, stripQueryAndHash, } from "./propertyPanelHelpers"; +import { FlatSelectRow, FlatToggle } from "./propertyPanelFlatPrimitives"; +// fallow-ignore-next-line complexity export function FlatMediaSection({ projectDir, element, @@ -43,10 +45,8 @@ export function FlatMediaSection({ const srcAttr = el.getAttribute("src") ?? ""; const [copied, setCopied] = useState(false); const [removeBusy, setRemoveBusy] = useState(false); - // oxlint-disable-next-line no-unused-vars -- rendered by the progress bar added in Task 3 const [removeProgress, setRemoveProgress] = useState(null); const [createPlate, setCreatePlate] = useState(false); - // oxlint-disable-next-line no-unused-vars -- wired into the Quality FlatSelectRow in Task 3 const [quality, setQuality] = useState<"fast" | "balanced" | "best">("balanced"); const absoluteSrc = @@ -55,7 +55,6 @@ export function FlatMediaSection({ srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr) ? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr) : ""; - // oxlint-disable-next-line no-unused-vars -- gates the Remove BG button added in Task 3 const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc); useEffect(() => { @@ -71,7 +70,6 @@ export function FlatMediaSection({ } }; - // oxlint-disable-next-line no-unused-vars -- called by the Remove BG button added in Task 3 const runBackgroundRemoval = async () => { if (!onRemoveBackground || !projectSrc || removeBusy) return; setRemoveBusy(true); @@ -120,6 +118,60 @@ export function FlatMediaSection({ {copied ? "Copied" : "Copy"} + {isVisualMedia && ( +
+
+ + Cutout + + transparent {isVideo ? "WebM" : "PNG"} + + + +
+ setQuality(next as typeof quality)} + /> + {isVideo && ( + + )} + {removeProgress && ( +
+
+ + {removeProgress.error ?? removeProgress.stage ?? "Processing"} + + {Math.round(removeProgress.progress)}% +
+
+
+
+
+ )} +
+ )}
); }