mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): add cutout sub-block to FlatMediaSection
This commit is contained in:
@@ -80,3 +80,52 @@ describe("FlatMediaSection — source row", () => {
|
|||||||
act(() => root.unmount());
|
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(
|
||||||
|
<FlatMediaSection
|
||||||
|
projectDir={null}
|
||||||
|
element={element}
|
||||||
|
styles={{}}
|
||||||
|
onSetStyle={vi.fn()}
|
||||||
|
onSetAttribute={onSetAttribute}
|
||||||
|
onSetHtmlAttribute={onSetHtmlAttribute}
|
||||||
|
onRemoveBackground={onRemoveBackground}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(host.textContent).toContain("transparent WebM");
|
||||||
|
const removeBgButton = host.querySelector<HTMLButtonElement>(
|
||||||
|
'[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<HTMLButtonElement>(
|
||||||
|
'[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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
type BackgroundRemovalResult,
|
type BackgroundRemovalResult,
|
||||||
stripQueryAndHash,
|
stripQueryAndHash,
|
||||||
} from "./propertyPanelHelpers";
|
} from "./propertyPanelHelpers";
|
||||||
|
import { FlatSelectRow, FlatToggle } from "./propertyPanelFlatPrimitives";
|
||||||
|
|
||||||
|
// fallow-ignore-next-line complexity
|
||||||
export function FlatMediaSection({
|
export function FlatMediaSection({
|
||||||
projectDir,
|
projectDir,
|
||||||
element,
|
element,
|
||||||
@@ -43,10 +45,8 @@ export function FlatMediaSection({
|
|||||||
const srcAttr = el.getAttribute("src") ?? "";
|
const srcAttr = el.getAttribute("src") ?? "";
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [removeBusy, setRemoveBusy] = 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<BackgroundRemovalProgress | null>(null);
|
const [removeProgress, setRemoveProgress] = useState<BackgroundRemovalProgress | null>(null);
|
||||||
const [createPlate, setCreatePlate] = useState(false);
|
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 [quality, setQuality] = useState<"fast" | "balanced" | "best">("balanced");
|
||||||
|
|
||||||
const absoluteSrc =
|
const absoluteSrc =
|
||||||
@@ -55,7 +55,6 @@ export function FlatMediaSection({
|
|||||||
srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr)
|
srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr)
|
||||||
? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : 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);
|
const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc);
|
||||||
|
|
||||||
useEffect(() => {
|
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 () => {
|
const runBackgroundRemoval = async () => {
|
||||||
if (!onRemoveBackground || !projectSrc || removeBusy) return;
|
if (!onRemoveBackground || !projectSrc || removeBusy) return;
|
||||||
setRemoveBusy(true);
|
setRemoveBusy(true);
|
||||||
@@ -120,6 +118,60 @@ export function FlatMediaSection({
|
|||||||
{copied ? "Copied" : "Copy"}
|
{copied ? "Copied" : "Copy"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{isVisualMedia && (
|
||||||
|
<div className="ml-[1px] border-l-2 border-panel-border-input py-1 pl-[10px]">
|
||||||
|
<div className="flex min-h-6 items-center justify-between">
|
||||||
|
<span className="flex items-baseline gap-[7px]">
|
||||||
|
<span className="text-[11px] font-semibold text-panel-text-1">Cutout</span>
|
||||||
|
<span className="font-mono text-[9px] text-panel-text-4">
|
||||||
|
transparent {isVideo ? "WebM" : "PNG"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-flat-media-remove-bg="true"
|
||||||
|
disabled={!canRemoveBackground || removeBusy}
|
||||||
|
onClick={() => void runBackgroundRemoval()}
|
||||||
|
className="flex items-center gap-1 text-[10px] font-medium text-panel-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
title={
|
||||||
|
canRemoveBackground
|
||||||
|
? "Remove background and save a transparent asset"
|
||||||
|
: "Select a project-local image or video asset"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{removeBusy ? "Working" : "Remove BG"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<FlatSelectRow
|
||||||
|
label="Quality"
|
||||||
|
value={quality}
|
||||||
|
options={["fast", "balanced", "best"]}
|
||||||
|
tier="explicitDefault"
|
||||||
|
onChange={(next) => setQuality(next as typeof quality)}
|
||||||
|
/>
|
||||||
|
{isVideo && (
|
||||||
|
<FlatToggle label="BG plate" checked={createPlate} onChange={setCreatePlate} />
|
||||||
|
)}
|
||||||
|
{removeProgress && (
|
||||||
|
<div className="mt-1 space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-[10px] text-panel-text-4">
|
||||||
|
<span className="min-w-0 flex-1 truncate">
|
||||||
|
{removeProgress.error ?? removeProgress.stage ?? "Processing"}
|
||||||
|
</span>
|
||||||
|
<span>{Math.round(removeProgress.progress)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1 overflow-hidden rounded-full bg-panel-hover">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full ${
|
||||||
|
removeProgress.status === "failed" ? "bg-red-400" : "bg-panel-accent"
|
||||||
|
}`}
|
||||||
|
style={{ width: `${Math.max(0, Math.min(100, removeProgress.progress))}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user