diff --git a/packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx
index f4b12ecbf..90767fec4 100644
--- a/packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx
+++ b/packages/studio/src/components/editor/propertyPanelFlatStyleSections.test.tsx
@@ -316,7 +316,8 @@ describe("FlatStyleSection — blur sliders", () => {
expect(host.textContent).toContain("Backdrop");
expect(host.textContent).toContain("6px");
const tracks = host.querySelectorAll('[data-flat-slider-track="true"]');
- const backdropTrack = tracks[tracks.length - 1];
+ // Track order is Layer blur, Backdrop, Opacity — Backdrop is the second track.
+ const backdropTrack = tracks[1];
Object.defineProperty(backdropTrack, "getBoundingClientRect", {
value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
});
@@ -330,7 +331,13 @@ describe("FlatStyleSection — blur sliders", () => {
it("does not render a fill/knob highlight for a zero-value blur (default tier)", () => {
const { host, root } = renderSection({});
- expect(host.querySelectorAll('[data-flat-slider-fill="true"]')).toHaveLength(0);
+ const tracks = host.querySelectorAll('[data-flat-slider-track="true"]');
+ // Only the first two tracks are the blur sliders (Layer blur, Backdrop); Opacity
+ // (the third track) always renders a fill by design, so it's excluded here.
+ const blurTracks = Array.from(tracks).slice(0, 2);
+ for (const track of blurTracks) {
+ expect(track.querySelectorAll('[data-flat-slider-fill="true"]')).toHaveLength(0);
+ }
act(() => root.unmount());
});
});
@@ -466,3 +473,36 @@ describe("FlatStyleSection — Overflow and Mask", () => {
act(() => root.unmount());
});
});
+
+describe("FlatStyleSection — Opacity", () => {
+ it("renders the Opacity slider at 100% by default and commits a change through onSetStyle", () => {
+ const onSetStyle = vi.fn();
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ act(() => {
+ root.render(
+ ,
+ );
+ });
+ expect(host.textContent).toContain("Opacity");
+ expect(host.textContent).toContain("100%");
+ const tracks = host.querySelectorAll('[data-flat-slider-track="true"]');
+ const opacityTrack = tracks[tracks.length - 1];
+ Object.defineProperty(opacityTrack, "getBoundingClientRect", {
+ value: () => ({ left: 0, width: 100, top: 0, height: 2, right: 100, bottom: 2 }),
+ });
+ act(() => {
+ opacityTrack.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 50 }));
+ });
+ expect(onSetStyle).toHaveBeenCalledWith("opacity", "0.5");
+ act(() => root.unmount());
+ });
+});
diff --git a/packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx b/packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx
index 8878409e1..0819ae660 100644
--- a/packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx
+++ b/packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx
@@ -477,6 +477,35 @@ function FlatOverflowMaskRows({
);
}
+/* ------------------------------------------------------------------ */
+/* Flat Opacity slider */
+/* ------------------------------------------------------------------ */
+
+function FlatOpacitySlider({
+ styles,
+ disabled,
+ onSetStyle,
+}: {
+ styles: Record;
+ disabled: boolean;
+ onSetStyle: (prop: string, value: string) => void | Promise;
+}) {
+ const opacityValue = Math.round((parseNumericValue(styles.opacity) ?? 1) * 100);
+
+ return (
+ void onSetStyle("opacity", formatNumericValue(next / 100))}
+ />
+ );
+}
+
export function FlatStyleSection({
projectId,
element,
@@ -523,6 +552,7 @@ export function FlatStyleSection({
disabled={styleEditingDisabled}
onSetStyle={onSetStyle}
/>
+
);
}