diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx index 33b71d41e..8b2e2fef5 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx @@ -7,6 +7,7 @@ import { FlatGroup, FlatRow, FlatSegmentedRow, + FlatSlider, PinnedZoneDivider, } from "./propertyPanelFlatPrimitives"; @@ -157,3 +158,71 @@ describe("PinnedZoneDivider", () => { act(() => root.unmount()); }); }); + +describe("FlatSlider", () => { + it("renders the default tier with a dim knob at the correct position", () => { + const { host, root } = renderInto( + , + ); + const knob = host.querySelector('[data-flat-slider-knob="true"]'); + expect(knob).not.toBeNull(); + expect(knob?.className).toContain("bg-panel-text-4"); + expect(knob?.style.left).toBe("0%"); + const value = host.querySelector('[data-flat-slider-value="true"]'); + expect(value?.className).toContain("text-panel-text-3"); + expect(value?.textContent).toBe("0px"); + act(() => root.unmount()); + }); + + it("renders the explicitCustom tier with a filled track and bright knob", () => { + const { host, root } = renderInto( + , + ); + const fill = host.querySelector('[data-flat-slider-fill="true"]'); + expect(fill?.style.width).toBe("100%"); + const knob = host.querySelector('[data-flat-slider-knob="true"]'); + expect(knob?.className).toContain("bg-white"); + act(() => root.unmount()); + }); + + it("commits a value on track click, proportional to click position", () => { + const onCommit = vi.fn(); + const { host, root } = renderInto( + , + ); + const track = host.querySelector('[data-flat-slider-track="true"]'); + if (!track) throw new Error("expected a track element"); + Object.defineProperty(track, "getBoundingClientRect", { + value: () => ({ left: 0, width: 200, top: 0, height: 2, right: 200, bottom: 2 }), + }); + act(() => { + track.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true, clientX: 100 })); + }); + expect(onCommit).toHaveBeenCalledWith(50); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx index 067ecb7a7..a4b69df14 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx @@ -233,3 +233,81 @@ export function PinnedZoneDivider() { ); } + +/* ------------------------------------------------------------------ */ +/* FlatSlider — full-width label/track/value row */ +/* ------------------------------------------------------------------ */ + +export function FlatSlider({ + label, + value, + min, + max, + step = 1, + tier, + displayValue, + disabled, + onCommit, +}: { + label: string; + value: number; + min: number; + max: number; + step?: number; + tier: "default" | "explicitCustom"; + displayValue: string; + disabled?: boolean; + onCommit: (nextValue: number) => void; +}) { + const clampedPct = Math.max(0, Math.min(100, ((value - min) / Math.max(max - min, 1e-6)) * 100)); + + const commitFromClientX = (clientX: number, rect: DOMRect) => { + const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / Math.max(rect.width, 1))); + const raw = min + ratio * (max - min); + const stepped = Math.round(raw / step) * step; + onCommit(Math.max(min, Math.min(max, stepped))); + }; + + return ( + + {label} + { + if (disabled) return; + commitFromClientX(e.clientX, e.currentTarget.getBoundingClientRect()); + }} + > + {tier === "explicitCustom" && ( + + )} + + + + {displayValue} + + + ); +}