mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): add FlatSlider primitive for the flat inspector
This commit is contained in:
@@ -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(
|
||||
<FlatSlider
|
||||
label="Layer blur"
|
||||
value={0}
|
||||
min={0}
|
||||
max={40}
|
||||
tier="default"
|
||||
displayValue="0px"
|
||||
onCommit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const knob = host.querySelector<HTMLElement>('[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(
|
||||
<FlatSlider
|
||||
label="Opacity"
|
||||
value={100}
|
||||
min={0}
|
||||
max={100}
|
||||
tier="explicitCustom"
|
||||
displayValue="100%"
|
||||
onCommit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const fill = host.querySelector<HTMLElement>('[data-flat-slider-fill="true"]');
|
||||
expect(fill?.style.width).toBe("100%");
|
||||
const knob = host.querySelector<HTMLElement>('[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(
|
||||
<FlatSlider
|
||||
label="Opacity"
|
||||
value={50}
|
||||
min={0}
|
||||
max={100}
|
||||
tier="explicitCustom"
|
||||
displayValue="50%"
|
||||
onCommit={onCommit}
|
||||
/>,
|
||||
);
|
||||
const track = host.querySelector<HTMLElement>('[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());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -233,3 +233,81 @@ export function PinnedZoneDivider() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 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 (
|
||||
<div className="flex min-h-[28px] items-center gap-2.5">
|
||||
<span className="w-[86px] flex-shrink-0 text-[11px] text-panel-text-3">{label}</span>
|
||||
<div
|
||||
data-flat-slider-track="true"
|
||||
role="slider"
|
||||
aria-label={label}
|
||||
aria-valuenow={value}
|
||||
aria-disabled={disabled}
|
||||
className={`relative h-0.5 flex-1 rounded-full bg-panel-hover ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||
}`}
|
||||
onPointerDown={(e) => {
|
||||
if (disabled) return;
|
||||
commitFromClientX(e.clientX, e.currentTarget.getBoundingClientRect());
|
||||
}}
|
||||
>
|
||||
{tier === "explicitCustom" && (
|
||||
<div
|
||||
data-flat-slider-fill="true"
|
||||
className="absolute inset-y-0 left-0 rounded-full bg-panel-text-5"
|
||||
style={{ width: `${clampedPct}%` }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
data-flat-slider-knob="true"
|
||||
className={`absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full ${
|
||||
tier === "explicitCustom" ? "h-2 w-2 bg-white" : "h-[7px] w-[7px] bg-panel-text-4"
|
||||
}`}
|
||||
style={{ left: `${clampedPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
data-flat-slider-value="true"
|
||||
className={`w-11 flex-shrink-0 text-right font-mono text-[10px] ${
|
||||
tier === "explicitCustom" ? "text-panel-text-0" : "text-panel-text-3"
|
||||
}`}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user