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} /> + ); }