feat(studio): add FlatSelectRow primitive for the flat inspector

This commit is contained in:
Vance Ingalls
2026-07-14 15:50:37 -07:00
parent 9854c6de71
commit f71fcf4da0
2 changed files with 124 additions and 0 deletions
@@ -7,6 +7,7 @@ import {
FlatGroup,
FlatRow,
FlatSegmentedRow,
FlatSelectRow,
FlatSlider,
PinnedZoneDivider,
} from "./propertyPanelFlatPrimitives";
@@ -226,3 +227,62 @@ describe("FlatSlider", () => {
act(() => root.unmount());
});
});
describe("FlatSelectRow", () => {
it("renders the default tier with no reset button", () => {
const { host, root } = renderInto(
<FlatSelectRow
label="Blend"
value="normal"
options={["normal", "multiply", "screen"]}
tier="default"
onChange={vi.fn()}
/>,
);
const select = host.querySelector("select");
expect(select?.value).toBe("normal");
expect(host.querySelector('[data-flat-select-reset="true"]')).toBeNull();
act(() => root.unmount());
});
it("renders the explicitCustom tier with a reset button and fires onReset", () => {
const onReset = vi.fn();
const { host, root } = renderInto(
<FlatSelectRow
label="Shadow"
value="soft"
options={["none", "soft", "lift", "glow"]}
tier="explicitCustom"
onChange={vi.fn()}
onReset={onReset}
/>,
);
const select = host.querySelector<HTMLSelectElement>("select");
expect(select?.className).toContain("text-panel-accent");
const reset = host.querySelector<HTMLButtonElement>('[data-flat-select-reset="true"]');
act(() => reset?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onReset).toHaveBeenCalledTimes(1);
act(() => root.unmount());
});
it("fires onChange when the select value changes", () => {
const onChange = vi.fn();
const { host, root } = renderInto(
<FlatSelectRow
label="Overflow"
value="visible"
options={["visible", "hidden", "clip", "auto", "scroll"]}
tier="default"
onChange={onChange}
/>,
);
const select = host.querySelector<HTMLSelectElement>("select");
if (!select) throw new Error("expected a select");
act(() => {
select.value = "hidden";
select.dispatchEvent(new Event("change", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledWith("hidden");
act(() => root.unmount());
});
});
@@ -311,3 +311,67 @@ export function FlatSlider({
</div>
);
}
/* ------------------------------------------------------------------ */
/* FlatSelectRow — label/value row backed by a native <select> */
/* ------------------------------------------------------------------ */
export function FlatSelectRow({
label,
value,
options,
tier,
disabled,
onChange,
onReset,
}: {
label: string;
value: string;
options: string[];
tier: PropertyValueTier;
disabled?: boolean;
onChange: (nextValue: string) => void;
onReset?: () => void;
}) {
return (
<div className="group flex min-h-[30px] items-center justify-between">
<span className={`text-[11px] ${VALUE_TIER_LABEL_CLASS[tier]}`}>{label}</span>
<span className="flex items-center gap-2">
<label className="flex items-center gap-1.5">
<select
value={value}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
className={`appearance-none bg-transparent text-right font-mono text-[11px] outline-none disabled:cursor-not-allowed ${VALUE_TIER_VALUE_CLASS[tier]}`}
>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="currentColor"
className="flex-shrink-0 text-panel-text-5"
>
<path d="M2 3l3 4 3-4z" />
</svg>
</label>
{tier === "explicitCustom" && onReset && (
<button
type="button"
data-flat-select-reset="true"
title="Remove — fall back to default"
onClick={onReset}
className="flex-shrink-0 text-panel-text-3 opacity-0 transition-opacity hover:text-panel-text-1 group-hover:opacity-100"
>
<RotateCcw size={11} />
</button>
)}
</span>
</div>
);
}