feat(studio): wrap FX parameter names instead of truncating them

The reverb details column read "How big the sp…", "How soft the wa…", "How much
origi…" — three rows whose visible text was nearly the same four words. These
names are whole questions, so 86px of truncation removes the part that tells them
apart, and a `title` only answers one row at a time on hover.

Same treatment the timeline gutter names got in 46ca2f3e5: `truncate` +
`title={param.label}` becomes `break-words leading-tight`, and the title goes —
wrapping answers the whole column at rest. The row keeps `title={param.hint}`;
the name and the explanation are different questions, and the hint was never
what got cut.

Applied to both FxParamRow shapes (numeric and enum) and to the carve module's
"Listen to", which sits in the same column — one truncating row beside wrapping
ones reads as a rendering bug.

Measured in the running studio on a reverb node: four rows at 25/25/24/25px, two
lines where the name needs them, one where it does not, no ellipsis. The two
tests that asserted the old title now assert the wrap (full text present,
`break-words` set, `truncate` absent, no `title`, hint still on the row).

171 tests across the three FX suites pass.
This commit is contained in:
Vance Ingalls
2026-08-20 16:41:10 -07:00
parent 5d0c9827aa
commit 373884ddc0
3 changed files with 24 additions and 23 deletions
@@ -336,10 +336,9 @@ function CarveSourceRow({
}) {
return (
<div className="hf-fx-row flex min-h-6 items-center gap-2">
<span
className="hf-fx-label w-[86px] flex-shrink-0 truncate text-[10px] text-panel-text-2"
title="Listen to"
>
{/* Wraps like every other name in this column (see FxParamRow) — one
truncating row beside wrapping ones reads as a rendering bug. */}
<span className="hf-fx-label w-[86px] flex-shrink-0 break-words text-[10px] leading-tight text-panel-text-2">
Listen to
</span>
{soleVoice ? (
@@ -131,14 +131,19 @@ describe("FxParamRow label tooltip", () => {
hint: "How long before the echo comes back.",
};
it("titles the label with the full name, and leaves the hint on the row", () => {
it("wraps the full name rather than truncating it, and leaves the hint on the row", () => {
const host = document.createElement("div");
document.body.append(host);
act(() => {
createRoot(host).render(<FxParamRow param={LONG} value={250} onChange={vi.fn()} />);
});
const label = host.querySelector<HTMLElement>(".hf-fx-label");
expect(label?.getAttribute("title")).toBe("Gap between repeats");
// The whole name is present and allowed to wrap. No `title`: a tooltip
// answers one row on hover, wrapping answers the column at rest.
expect(label?.textContent).toBe("Gap between repeats");
expect(label?.className).toContain("break-words");
expect(label?.className).not.toContain("truncate");
expect(label?.getAttribute("title")).toBeNull();
// The hint stays where it was: the two are different questions, and the
// name is not a substitute for the explanation either.
expect(host.querySelector<HTMLElement>(".hf-fx-row")?.getAttribute("title")).toBe(
@@ -146,15 +151,15 @@ describe("FxParamRow label tooltip", () => {
);
});
it("titles it even with no hint to fall back on", () => {
it("wraps it the same way with no hint to fall back on", () => {
const host = document.createElement("div");
document.body.append(host);
const { hint: _hint, ...noHint } = LONG;
act(() => {
createRoot(host).render(<FxParamRow param={noHint} value={250} onChange={vi.fn()} />);
});
expect(host.querySelector<HTMLElement>(".hf-fx-label")?.getAttribute("title")).toBe(
"Gap between repeats",
);
const label = host.querySelector<HTMLElement>(".hf-fx-label");
expect(label?.textContent).toBe("Gap between repeats");
expect(label?.getAttribute("title")).toBeNull();
});
});
@@ -188,15 +188,14 @@ export function FxParamRow({
if (param.kind === "enum") {
return (
<label className="hf-fx-row flex min-h-6 items-center gap-2" title={param.hint}>
{/* `title` on the LABEL, not just the row: the row's is the hint, which
explains what the knob does — useful, and no substitute for the name
when 86px truncates it to "Gap between re…". Titled unconditionally
rather than only when it overflows, since whether it does depends on
the rendered font and the panel's width. */}
<span
className="hf-fx-label w-[86px] flex-shrink-0 truncate text-[10px] text-panel-text-2"
title={param.label}
>
{/* Wraps rather than truncating. These names are whole questions — "How
big the space is" — so 86px of truncation left "How big the sp…", and
three rows of that read as the same word four times. A title only
answers it on hover, one row at a time; wrapping answers it for the
whole column at rest. `break-words` so a long single token breaks
instead of widening the column. The row keeps `title={param.hint}`:
the name and the explanation are different questions. */}
<span className="hf-fx-label w-[86px] flex-shrink-0 break-words text-[10px] leading-tight text-panel-text-2">
{param.label}
</span>
<select
@@ -235,13 +234,11 @@ export function FxParamRow({
title={param.hint}
data-automated={automated ? "" : undefined}
>
{/* See the enum row above for why the name is titled here as well as the
hint being titled on the row. */}
{/* See the enum row above for why the name wraps instead of truncating. */}
<span
className={`hf-fx-label w-[86px] flex-shrink-0 truncate text-[10px] ${
className={`hf-fx-label w-[86px] flex-shrink-0 break-words text-[10px] leading-tight ${
automated ? "text-panel-accent" : "text-panel-text-2"
}`}
title={param.label}
>
{param.label}
</span>