feat(studio): add flat Shadow and Blend rows to the Style group

This commit is contained in:
Vance Ingalls
2026-07-09 11:52:14 -07:00
parent fbc3036a54
commit 1d7d8aaa5d
2 changed files with 118 additions and 1 deletions
@@ -214,3 +214,66 @@ describe("FlatStyleSection — Stroke and Radius", () => {
act(() => root.unmount());
});
});
function getFlatSelectRow(host: HTMLElement, label: string) {
const rows = Array.from(host.querySelectorAll<HTMLElement>(".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<HTMLSelectElement>("select");
if (!select) throw new Error(`expected a select for "${label}"`);
const resetButton = row.querySelector<HTMLButtonElement>('[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());
});
});
@@ -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<string, string>;
disabled: boolean;
onSetStyle: (prop: string, value: string) => void | Promise<void>;
}) {
const boxShadowPreset = inferBoxShadowPreset(styles["box-shadow"]);
const blendValue = styles["mix-blend-mode"] || "normal";
return (
<>
<FlatSelectRow
label="Shadow"
value={boxShadowPreset}
options={["none", "soft", "lift", "glow", "custom"]}
tier={resolveValueTier(boxShadowPreset === "none" ? undefined : boxShadowPreset, "none")}
disabled={disabled}
onChange={(next) => {
if (next === "custom") return;
void onSetStyle(
"box-shadow",
buildBoxShadowPresetValue(next as BoxShadowPreset, styles["box-shadow"]),
);
}}
onReset={() => void onSetStyle("box-shadow", "none")}
/>
<FlatSelectRow
label="Blend"
value={blendValue}
options={["normal", "multiply", "screen", "overlay", "darken", "lighten"]}
tier={resolveValueTier(styles["mix-blend-mode"], "normal")}
disabled={disabled}
onChange={(next) => 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}
/>
<FlatShadowBlendRows
styles={styles}
disabled={styleEditingDisabled}
onSetStyle={onSetStyle}
/>
</div>
);
}