diff --git a/packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx index 81f46b46d..6b47c7ddb 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx @@ -214,3 +214,66 @@ describe("FlatStyleSection — Stroke and Radius", () => { act(() => root.unmount()); }); }); + +function getFlatSelectRow(host: HTMLElement, label: string) { + const rows = Array.from(host.querySelectorAll(".group")); + const row = rows.find((el) => el.querySelector("span")?.textContent === label); + if (!row) throw new Error(`expected a select row for "${label}"`); + const select = row.querySelector("select"); + if (!select) throw new Error(`expected a select for "${label}"`); + const resetButton = row.querySelector('[data-flat-select-reset="true"]'); + return { row, select, resetButton }; +} + +function changeFlatSelectRow(host: HTMLElement, label: string, nextValue: string) { + const { select } = getFlatSelectRow(host, label); + act(() => { + select.value = nextValue; + select.dispatchEvent(new Event("change", { bubbles: true })); + }); +} + +describe("FlatStyleSection — Shadow and Blend", () => { + it("renders Shadow with the inferred preset and a reset when set, Blend with a plain select", () => { + const { host, root } = renderSection({ "box-shadow": "0 8px 24px rgba(0,0,0,.35)" }); + expect(host.textContent).toContain("Shadow"); + expect(host.textContent).toContain("Blend"); + const selects = host.querySelectorAll("select"); + expect(selects.length).toBeGreaterThanOrEqual(2); + act(() => root.unmount()); + }); + + it("commits a shadow preset change through onSetStyle", () => { + const { host, root, onSetStyle } = renderSection({}); + changeFlatSelectRow(host, "Shadow", "soft"); + expect(onSetStyle).toHaveBeenCalledWith("box-shadow", expect.any(String)); + act(() => root.unmount()); + }); + + it("commits a blend mode change through onSetStyle", () => { + const { host, root, onSetStyle } = renderSection({}); + changeFlatSelectRow(host, "Blend", "multiply"); + expect(onSetStyle).toHaveBeenCalledWith("mix-blend-mode", "multiply"); + act(() => root.unmount()); + }); + + it("resets the shadow preset back to none via the reset button", () => { + const { host, root, onSetStyle } = renderSection({ + "box-shadow": "0 12px 36px rgba(0, 0, 0, 0.28)", + }); + const { resetButton } = getFlatSelectRow(host, "Shadow"); + if (!resetButton) throw new Error("expected the shadow reset button"); + act(() => resetButton.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onSetStyle).toHaveBeenCalledWith("box-shadow", "none"); + act(() => root.unmount()); + }); + + it("resets the blend mode back to normal via the reset button", () => { + const { host, root, onSetStyle } = renderSection({ "mix-blend-mode": "multiply" }); + const { resetButton } = getFlatSelectRow(host, "Blend"); + if (!resetButton) throw new Error("expected the blend reset button"); + act(() => resetButton.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onSetStyle).toHaveBeenCalledWith("mix-blend-mode", "normal"); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx b/packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx index 24a586e29..b2f45548d 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx @@ -6,16 +6,19 @@ import { Link as LinkIcon } from "../../icons/SystemIcons"; import { BorderRadiusEditor } from "./BorderRadiusEditor"; import { formatStrokeSummary, parseStrokeSummary } from "./propertyPanelFlatStyleHelpers"; import { + buildBoxShadowPresetValue, buildStrokeStyleUpdates, buildStrokeWidthStyleUpdates, extractBackgroundImageUrl, formatNumericValue, formatPxMetricValue, + inferBoxShadowPreset, normalizePanelPxValue, parseNumericValue, parsePxMetricValue, + type BoxShadowPreset, } from "./propertyPanelHelpers"; -import { FlatRow, FlatSegmentedRow } from "./propertyPanelFlatPrimitives"; +import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives"; import { resolveValueTier } from "./propertyPanelValueTier"; import { ColorField } from "./propertyPanelColor"; import { GradientField, ImageFillField } from "./propertyPanelFill"; @@ -268,6 +271,52 @@ function FlatRadiusRow({ ); } +/* ------------------------------------------------------------------ */ +/* Flat Shadow + Blend rows */ +/* ------------------------------------------------------------------ */ + +function FlatShadowBlendRows({ + styles, + disabled, + onSetStyle, +}: { + styles: Record; + disabled: boolean; + onSetStyle: (prop: string, value: string) => void | Promise; +}) { + const boxShadowPreset = inferBoxShadowPreset(styles["box-shadow"]); + const blendValue = styles["mix-blend-mode"] || "normal"; + + return ( + <> + { + if (next === "custom") return; + void onSetStyle( + "box-shadow", + buildBoxShadowPresetValue(next as BoxShadowPreset, styles["box-shadow"]), + ); + }} + onReset={() => void onSetStyle("box-shadow", "none")} + /> + void onSetStyle("mix-blend-mode", next)} + onReset={() => void onSetStyle("mix-blend-mode", "normal")} + /> + + ); +} + export function FlatStyleSection({ projectId, element, @@ -303,6 +352,11 @@ export function FlatStyleSection({ disabled={styleEditingDisabled} onSetStyle={onSetStyle} /> + ); }