feat(studio): add FlatToggle primitive

This commit is contained in:
Vance Ingalls
2026-07-14 15:50:44 -07:00
parent 4b592832ab
commit 14d8de7492
2 changed files with 88 additions and 0 deletions
@@ -9,6 +9,7 @@ import {
FlatSegmentedRow,
FlatSelectRow,
FlatSlider,
FlatToggle,
PinnedZoneDivider,
} from "./propertyPanelFlatPrimitives";
@@ -286,3 +287,44 @@ describe("FlatSelectRow", () => {
act(() => root.unmount());
});
});
describe("FlatToggle", () => {
it("renders the off state with a dim label and dim knob, and fires onChange(true) on click", () => {
const onChange = vi.fn();
const { host, root } = renderInto(
<FlatToggle label="Loop" checked={false} onChange={onChange} />,
);
const label = host.querySelector('[data-flat-toggle-label="true"]');
expect(label?.className).toContain("text-panel-text-3");
const pill = host.querySelector<HTMLButtonElement>('[data-flat-toggle="true"]');
expect(pill).not.toBeNull();
act(() => pill?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onChange).toHaveBeenCalledWith(true);
act(() => root.unmount());
});
it("renders the on state with an emphasized label and mint knob, and fires onChange(false) on click", () => {
const onChange = vi.fn();
const { host, root } = renderInto(<FlatToggle label="Loop" checked onChange={onChange} />);
const label = host.querySelector('[data-flat-toggle-label="true"]');
expect(label?.className).toContain("text-panel-text-2");
const knob = host.querySelector('[data-flat-toggle-knob="true"]');
expect(knob?.className).toContain("bg-panel-accent");
const pill = host.querySelector<HTMLButtonElement>('[data-flat-toggle="true"]');
act(() => pill?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onChange).toHaveBeenCalledWith(false);
act(() => root.unmount());
});
it("does not fire onChange when disabled", () => {
const onChange = vi.fn();
const { host, root } = renderInto(
<FlatToggle label="Loop" checked={false} disabled onChange={onChange} />,
);
const pill = host.querySelector<HTMLButtonElement>('[data-flat-toggle="true"]');
expect(pill?.disabled).toBe(true);
act(() => pill?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onChange).not.toHaveBeenCalled();
act(() => root.unmount());
});
});
@@ -375,3 +375,49 @@ export function FlatSelectRow({
</div>
);
}
/* ------------------------------------------------------------------ */
/* FlatToggle — 24×14 pill switch */
/* ------------------------------------------------------------------ */
export function FlatToggle({
label,
checked,
disabled,
onChange,
}: {
label: string;
checked: boolean;
disabled?: boolean;
onChange: (next: boolean) => void;
}) {
return (
<div className="flex min-h-[30px] items-center justify-between">
<span
data-flat-toggle-label="true"
className={`text-[11px] ${checked ? "text-panel-text-2" : "text-panel-text-3"}`}
>
{label}
</span>
<button
type="button"
data-flat-toggle="true"
role="switch"
aria-checked={checked}
aria-label={label}
disabled={disabled}
onClick={() => onChange(!checked)}
className={`relative h-[14px] w-6 flex-shrink-0 rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
checked ? "bg-panel-accent/35" : "bg-panel-hover"
}`}
>
<span
data-flat-toggle-knob="true"
className={`absolute top-0.5 h-2.5 w-2.5 rounded-full transition-all ${
checked ? "right-0.5 bg-panel-accent" : "left-0.5 bg-panel-text-4"
}`}
/>
</button>
</div>
);
}