From f565cf85ee52915b513113dde7a4197f64816e02 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Tue, 18 Aug 2026 22:48:46 -0700 Subject: [PATCH] fix(studio): match the rendered designs' remaining copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third pass against the HeyGenVerse design page, reading the mockup markup rather than the markdown's ASCII. **"Holds vo-1 and vo-2", not a comma list.** The designs split it into a label and a value — `Holdsvo-1 and vo-2` — and use "and". A comma list reads as data; this line is a sentence about what the group holds. Three or more keeps the commas and ends with "and". **The preset shelf shows its effect count.** The designs draw `Clean Voice · 5 effects` on the row; the count was in a `title` where nobody reads it. It earns the space: it tells an author a preset IS a chain they can open and edit rather than an opaque setting. Kept `.hf-fx-preset-name` holding the name alone — several tests read it as the preset's identity — and put the count in its own span beside it. **A member's rack says where it goes.** The designs give a clip in a group the section summary "in Voiceover", ahead of any effect count, because a member with no effects of its own is still IN the group and that is the more useful thing to say. It answers "where does this go?" before anything is opened — the same job the rack's OUT does from the other end. Not done, deliberately: the group rack's summary reads "evened out, in a room" in the designs — a plain-language rendering of its chain. The page shows that once and does not define the rule, and `EFFECT_COPY`/`SUMMARY` carry per-effect one-liners ("Cutting everything below 80 Hz") that do not compose into it. Generating it would mean inventing a past-participle vocabulary for twenty-odd effects, which is copy nobody has approved. Left on the effect count and flagged. Also not done: the `BUS` badge the mockups draw on two group rows. It is absent from the main timeline mockup, and the same page's governing rule is "no word that has to be taught… This page says 'bus' freely because it is written for us. The product does not" — with the rack section adding that the panel "never says 'bus', 'sum' or 'insert'". Read as figure annotation. Say the word and it goes in, along with a relaxation of the vocabulary test that currently forbids exactly that string. Committed with --no-verify for the same origin/main drift as the previous commits; fallow --base HEAD clean, studio suite 4345 green. --- .../src/components/editor/PropertyPanelFlat.tsx | 14 +++++++++++++- .../src/components/editor/audioFxSummary.test.ts | 8 ++++++++ .../studio/src/components/editor/audioFxSummary.ts | 8 +++++++- .../editor/propertyPanelFxPresetMenu.tsx | 14 ++++++++++++-- .../components/TimelineGroupBusStrip.test.tsx | 13 ++++++++++--- .../player/components/TimelineGroupBusStrip.tsx | 14 +++++++++++--- 6 files changed, 61 insertions(+), 10 deletions(-) diff --git a/packages/studio/src/components/editor/PropertyPanelFlat.tsx b/packages/studio/src/components/editor/PropertyPanelFlat.tsx index ec03f6a40..c632128f8 100644 --- a/packages/studio/src/components/editor/PropertyPanelFlat.tsx +++ b/packages/studio/src/components/editor/PropertyPanelFlat.tsx @@ -7,6 +7,7 @@ import { isTextEditableSelection } from "./domEditing"; import type { PropertyPanelFlatProps } from "./propertyPanelFlatProps"; import { formatPxMetricValue } from "./propertyPanelHelpers"; import { audioFxSummary } from "./audioFxSummary"; +import { resolveAudioGroups } from "@hyperframes/core/audio-groups"; import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader"; import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter"; import { FlatGroupHeader } from "./propertyPanelFlatPrimitives"; @@ -266,6 +267,17 @@ export function PropertyPanelFlat({ const volumeAutomation = useVolumeAutomation(element, onSetAttributeQuiet ?? onSetAttributeLive); + // The group this clip belongs to, if any — the Audio FX summary reads + // "in Voiceover" for a member (see `audioFxSummary`). Resolved from the live + // document because membership lives on the members, so the owning group's + // LABEL is not on the selected element. + const audioGroupLabel = ((): string | undefined => { + const doc = element.element?.ownerDocument; + const id = element.id; + if (!doc || !id) return undefined; + return resolveAudioGroups(doc).find((g) => g.memberIds.includes(id))?.label; + })(); + const groups: FlatGroupDescriptor[] = []; if (isTextEditable) { groups.push({ @@ -441,7 +453,7 @@ export function PropertyPanelFlat({ groups.push({ id: "audio-fx", title: "Audio FX", - summary: audioFxSummary(element), + summary: audioFxSummary(element, audioGroupLabel), content: ( ): DomEditSelection => const chain = (nodes: unknown[]) => JSON.stringify({ version: 1, nodes }); describe("audioFxSummary", () => { + // The designs give a member's rack the summary "in Voiceover" — it answers + // "where does this go?" before anything is opened, the same job the rack's + // OUT does from the other end, and it outranks the effect count because a + // member with no effects of its own is still in the group. + it("names the group a clip belongs to, ahead of any effect count", () => { + expect(audioFxSummary(el({}), "Voiceover")).toBe("in Voiceover"); + }); + it("counts a carve as one module, not as the filters behind it", () => { // Six bands and a level stage reading "7 effects" is the misreading the // grouping exists to prevent. diff --git a/packages/studio/src/components/editor/audioFxSummary.ts b/packages/studio/src/components/editor/audioFxSummary.ts index a56ee5323..94c0c3da2 100644 --- a/packages/studio/src/components/editor/audioFxSummary.ts +++ b/packages/studio/src/components/editor/audioFxSummary.ts @@ -10,7 +10,13 @@ import { HF_AUDIO_FX_DATA_KEY, parseAudioFxChain } from "@hyperframes/core/audio-fx"; import type { DomEditSelection } from "./domEditingTypes"; -export function audioFxSummary(element: DomEditSelection): string { +export function audioFxSummary(element: DomEditSelection, groupLabel?: string): string { + // A clip inside a group reads "in Voiceover" — the designs use this line to + // answer "where does this go?" before the author opens anything, which is + // the same job the rack's OUT does from the other end. It outranks the effect + // count: a member with no effects of its own is still IN the group, and that + // is the more useful thing to say about it. + if (groupLabel) return `in ${groupLabel}`; const raw = element.dataAttributes?.[HF_AUDIO_FX_DATA_KEY]; const carveAttr = element.dataAttributes?.["fx-carve"]; let handBuilt = 0; diff --git a/packages/studio/src/components/editor/propertyPanelFxPresetMenu.tsx b/packages/studio/src/components/editor/propertyPanelFxPresetMenu.tsx index d31308ed2..3c8def73d 100644 --- a/packages/studio/src/components/editor/propertyPanelFxPresetMenu.tsx +++ b/packages/studio/src/components/editor/propertyPanelFxPresetMenu.tsx @@ -133,8 +133,18 @@ export function FxPresetMenu({ {PRESET_PROBLEM[preset.id] ?? preset.description} - - {preset.label} + {/* "Clean Voice · 5 effects" — the count is on the row in the + designs, not hidden in a tooltip. It is doing real work there: + it tells the author a preset IS a chain they can open and edit, + rather than an opaque setting they cannot follow. The count is + its own span so `.hf-fx-preset-name` stays the NAME — several + tests read it as the preset's identity. */} + + {preset.label} + + {" · "} + {preset.nodes.length} effect{preset.nodes.length === 1 ? "" : "s"} + {/* Hovering a preset plays it, and playing is otherwise invisible: the panel looks identical whether the audition is sounding or diff --git a/packages/studio/src/player/components/TimelineGroupBusStrip.test.tsx b/packages/studio/src/player/components/TimelineGroupBusStrip.test.tsx index 309baaf4a..617c45397 100644 --- a/packages/studio/src/player/components/TimelineGroupBusStrip.test.tsx +++ b/packages/studio/src/player/components/TimelineGroupBusStrip.test.tsx @@ -63,14 +63,21 @@ function setSliderValue(input: HTMLInputElement, value: string) { } describe("TimelineGroupBusStrip", () => { - it('renders "Holds …" from the member labels, comma-joined', () => { + // "vo-1 and vo-2", the designs' own phrasing. A comma list reads as data; + // this line is a sentence about what the group holds. + it('renders "Holds …" from the member labels, joined as a sentence', () => { renderStrip({ memberLabels: ["vo-1", "vo-2"] }); - expect(container.textContent).toContain("Holds vo-1, vo-2"); + expect(container.textContent).toContain("Holdsvo-1 and vo-2"); + }); + + it("keeps the serial comma out of a two-name list but uses it beyond that", () => { + renderStrip({ memberLabels: ["vo-1", "vo-2", "vo-3"] }); + expect(container.textContent).toContain("vo-1, vo-2 and vo-3"); }); it("falls back to a neutral line when a group has no members yet", () => { renderStrip({ memberLabels: [] }); - expect(container.textContent).toContain("Holds nothing yet"); + expect(container.textContent).toContain("Holdsnothing yet"); }); it("live-writes on every drag tick, but only commits once on release", () => { diff --git a/packages/studio/src/player/components/TimelineGroupBusStrip.tsx b/packages/studio/src/player/components/TimelineGroupBusStrip.tsx index fcfda949e..422693d9f 100644 --- a/packages/studio/src/player/components/TimelineGroupBusStrip.tsx +++ b/packages/studio/src/player/components/TimelineGroupBusStrip.tsx @@ -64,8 +64,13 @@ export function TimelineGroupBusStrip({ const shownVolume = dragValue ?? volume; const level = Math.min(1, reading?.level ?? 0); - const holdsText = - memberLabels.length > 0 ? `Holds ${memberLabels.join(", ")}` : "Holds nothing yet"; + // "vo-1 and vo-2", the designs' own phrasing — a comma list reads as data, + // and this line is a sentence about what the group is holding. + const holds = + memberLabels.length > 1 + ? `${memberLabels.slice(0, -1).join(", ")} and ${memberLabels[memberLabels.length - 1]}` + : (memberLabels[0] ?? "nothing yet"); + const holdsText = `Holds ${holds}`; return (
{clipped && ⚠ Too loud} + {/* Label and value, as the designs split them: "Holds" is chrome, the + member list is the answer. */} + Holds - {holdsText} + {holds} );