Skip to content

Commit 3fff5f3

Browse files
committed
feat(studio): title the carve, hide voice presets off voice tracks, fix panel contrast
Three things, all found by opening the rack and looking at it. **The carve gets a title treatment.** Presets got one because each is a character; the carve gets one because it is the only module in the rack that LISTENS to another track, and a plain 11px row understated that. It keeps the smart family's monospace — what it shows is a readout, and a display face would promise settings the author chose — but takes the family tint as its colour and the same near-black wash the presets use. The wash derivation moved out of `fxPresetBackground` into `fxTintWash`, which takes a colour rather than a preset id, because the smart modules have no catalogue entry to look up. That surfaced a real bug: the pattern only matched integer HSL, and `fxFamilyTint` COMPUTES its lightness, so it emits `62.0%`. The carve rendered with no wash at all. Caught in the browser, not by a test — there wasn't one. There is now, and it fails against the old pattern. **Voice presets no longer show on a music bed.** `classifyAudioName` was already deciding which tracks a carve may listen to; the same reading now tells the shelf what the selected track is. Only a confident "music" or "sfx" hides anything — "unknown" keeps everything, on the principle the source picker follows: a name is a hint, and hiding what somebody came for costs more than one shelf to scroll past. Verified both directions in the studio: #vo shows 4 families / 19 presets, #bgm shows 3 / 16. **Contrast.** Every small label in the rack used `panel-text-4` (#52525B), which measures 2.29:1 on the panel — WCAG AA wants 4.5 for text this size. 23 failing nodes. Moved the FX modules to `panel-text-2` (#A1A1AA, 6.9:1); left the token alone, since it is studio-wide across 35 files and redefining it is a different change. Re-measured across the shelf, the collapsed rack, the expanded module bodies and a populated carve: 0 failures.
1 parent 95140e8 commit 3fff5f3

11 files changed

Lines changed: 174 additions & 53 deletions

packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,10 @@ export function AudioFxGroup({
895895
onCarveChange={(next) => void setCarve(next)}
896896
onCarvePreview={(next) => onSetAttributeLive(HF_AUDIO_CARVE_ATTR, JSON.stringify(next))}
897897
sourceOptions={sourceOptions}
898+
// What this track sounds like, by the same reading that decides which
899+
// OTHER tracks a carve may listen to. The shelf uses it to stop offering
900+
// "My voice sounds amateur" on a music bed.
901+
trackKind={classifyAudioName(element.id, element.element?.getAttribute("src"))}
898902
onLevel={() => void runLeveller()}
899903
onRemoveLevel={removeLeveller}
900904
levelled={chain.nodes.some((n) => n.fromLeveller)}

packages/studio/src/components/editor/propertyPanelFxBandRuler.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function FxBandRuler({ band, at }: FxBandRulerProps) {
6464
);
6565
})}
6666
</div>
67-
<p className="hf-fx-ruler-label truncate pt-0.5 text-[9px] text-panel-text-4">
67+
<p className="hf-fx-ruler-label truncate pt-0.5 text-[9px] text-panel-text-2">
6868
<span className="hf-fx-ruler-name text-panel-text-1">{here.name}</span>{here.says}
6969
</p>
7070
</div>

packages/studio/src/components/editor/propertyPanelFxCarveModule.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { DEFAULT_CARVE, type HfCarveSettings } from "@hyperframes/core/audio-car
1818
import { fxAutomationTarget } from "@hyperframes/core/audio-automation";
1919
import { FxParamRow } from "./propertyPanelFxControls.js";
2020
import { FX_FAMILY_TYPE, fxFamilyTint } from "./propertyPanelFxFamily.js";
21+
import { fxTintWash } from "./propertyPanelFxPresetStyle.js";
2122
// Shared with the timeline's lane labels: a band is named by its frequency in
2223
// both places, and two formatters would drift.
2324
import { formatHz } from "../../player/components/automationLaneData";
@@ -96,11 +97,11 @@ function FxCarveMember({
9697
return (
9798
<span
9899
key={param.key}
99-
className="flex items-baseline gap-1 font-mono text-[9px] text-panel-text-4"
100+
className="flex items-baseline gap-1 font-mono text-[9px] text-panel-text-2"
100101
{...(automated ? { "data-automated": "" } : {})}
101102
{...(driven ? { "data-automation-live": "" } : {})}
102103
>
103-
<span className="text-panel-text-4">{param.label}</span>
104+
<span className="text-panel-text-2">{param.label}</span>
104105
<span
105106
className="tabular-nums text-panel-text-1"
106107
style={{ minWidth: `${paramValueWidthCh(param)}ch` }}
@@ -200,6 +201,14 @@ export function FxCarveModule({
200201
: carve.sources.length > 0
201202
? "no analysis yet"
202203
: "pick a voice";
204+
// The carve's own colour, used three ways: the module's left edge, the title,
205+
// and the wash behind it. A preset gets a title treatment because it is a
206+
// character; the carve gets one because it is the only module in the rack
207+
// that LISTENS to another track, and a plain row understates that. It stays
208+
// in the smart family's monospace — what it shows is a readout, and a
209+
// display face would promise settings the author chose.
210+
const tint = fxFamilyTint({ type: "carve", fromCarve: true });
211+
const wash = fxTintWash(tint);
203212
return (
204213
<div
205214
className={`hf-fx-node hf-fx-carve-module hf-fx-carve rounded-[4px] border border-l-2 border-panel-border-input${
@@ -210,27 +219,31 @@ export function FxCarveModule({
210219
// Smart, like the Tone EQ and the leveller: it measures the audio and
211220
// writes its own settings, and what it shows is a readout of what it
212221
// decided rather than controls the author set.
213-
style={{ borderLeftColor: fxFamilyTint({ type: "carve", fromCarve: true }) }}
222+
style={{ borderLeftColor: tint, ...(wash ? { backgroundColor: wash } : {}) }}
214223
data-carve-enabled={on ? "" : undefined}
215224
>
216225
<div className="hf-fx-node-head flex min-h-7 items-center gap-1 px-1.5">
217226
<button
218227
type="button"
219-
className={`hf-fx-node-name min-w-0 flex-1 truncate text-left text-[11px] text-panel-text-1 hover:text-panel-text-0 ${FX_FAMILY_TYPE.smart}`}
228+
className={`hf-fx-node-name min-w-0 flex-1 truncate text-left text-[13px] uppercase hover:opacity-80 ${FX_FAMILY_TYPE.smart}`}
229+
// Tracking goes here rather than in a class: the smart family already
230+
// sets `tracking-normal`, and two Tailwind tracking utilities on one
231+
// element resolve by stylesheet order, not by the order written.
232+
style={{ color: tint, letterSpacing: "0.16em" }}
220233
aria-expanded={open}
221234
onClick={onToggleOpen}
222235
>
223236
Voiceover carve
224237
</button>
225-
<span className="hf-fx-carve-summary shrink-0 font-mono text-[9px] text-panel-text-4">
238+
<span className="hf-fx-carve-summary shrink-0 font-mono text-[9px] text-panel-text-2">
226239
{summary}
227240
</span>
228241
{/* One switch, not a bypass and a delete. Off drops the effects and the
229242
envelopes it wrote, and is remembered — otherwise the default would
230243
re-apply the carve the next time this clip was selected. */}
231244
<button
232245
type="button"
233-
className="hf-fx-bypass hf-fx-carve-toggle rounded-[3px] border border-panel-border-input px-1.5 py-0.5 font-mono text-[9px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-40"
246+
className="hf-fx-bypass hf-fx-carve-toggle rounded-[3px] border border-panel-border-input px-1.5 py-0.5 font-mono text-[9px] text-panel-text-2 hover:text-panel-text-0 disabled:opacity-40"
234247
aria-pressed={on}
235248
title={on ? "Switch the carve off" : "Switch the carve on"}
236249
disabled={disabled}
@@ -243,7 +256,7 @@ export function FxCarveModule({
243256
<div className="hf-fx-carve-body border-t border-panel-border-input">
244257
<div className="hf-fx-carve-controls space-y-0.5 px-1.5 py-1.5">
245258
<div className="hf-fx-row flex min-h-6 items-center gap-2">
246-
<span className="hf-fx-label w-[86px] flex-shrink-0 truncate text-[10px] text-panel-text-4">
259+
<span className="hf-fx-label w-[86px] flex-shrink-0 truncate text-[10px] text-panel-text-2">
247260
Listen to
248261
</span>
249262
{soleVoice ? (
@@ -318,7 +331,7 @@ export function FxCarveModule({
318331
settings that are in force when they are already history, and the one
319332
honest thing to say is that the work is happening. */}
320333
{analysing ? (
321-
<p className="hf-fx-carve-working flex items-center justify-center gap-1.5 border-t border-panel-border-input py-2 text-[10px] text-panel-text-4">
334+
<p className="hf-fx-carve-working flex items-center justify-center gap-1.5 border-t border-panel-border-input py-2 text-[10px] text-panel-text-2">
322335
<svg
323336
className="hf-fx-carve-spinner h-3 w-3 animate-spin motion-reduce:animate-none"
324337
viewBox="0 0 24 24"
@@ -343,7 +356,7 @@ export function FxCarveModule({
343356
</p>
344357
) : nodes.length > 0 ? (
345358
<div className="hf-fx-carve-members divide-y divide-panel-border-input/60 border-t border-panel-border-input">
346-
<div className="hf-fx-carve-members-label px-1.5 pt-1 font-mono text-[9px] uppercase tracking-wide text-panel-text-4">
359+
<div className="hf-fx-carve-members-label px-1.5 pt-1 font-mono text-[9px] uppercase tracking-wide text-panel-text-2">
347360
analysed
348361
</div>
349362
{nodes.map((node, i) => (
@@ -356,7 +369,7 @@ export function FxCarveModule({
356369
))}
357370
</div>
358371
) : (
359-
<p className="hf-fx-carve-working border-t border-panel-border-input py-1.5 text-center text-[10px] text-panel-text-4">
372+
<p className="hf-fx-carve-working border-t border-panel-border-input py-1.5 text-center text-[10px] text-panel-text-2">
360373
{carve.sources.length > 0
361374
? "Nothing analysed yet."
362375
: "Pick the voices this bed should make room for."}

packages/studio/src/components/editor/propertyPanelFxControls.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function AutomationToggle({
9191
className={`hf-fx-automate w-[16px] flex-shrink-0 rounded-[3px] border font-mono text-[9px] leading-none ${
9292
automated
9393
? "border-panel-accent text-panel-accent"
94-
: "border-panel-border-input text-panel-text-4 hover:text-panel-text-0"
94+
: "border-panel-border-input text-panel-text-2 hover:text-panel-text-0"
9595
}`}
9696
aria-pressed={automated}
9797
aria-label={automated ? `Remove ${label} automation` : `Automate ${label}`}
@@ -188,7 +188,7 @@ export function FxParamRow({
188188
if (param.kind === "enum") {
189189
return (
190190
<label className="hf-fx-row flex min-h-6 items-center gap-2" title={param.hint}>
191-
<span className="hf-fx-label w-[86px] flex-shrink-0 truncate text-[10px] text-panel-text-4">
191+
<span className="hf-fx-label w-[86px] flex-shrink-0 truncate text-[10px] text-panel-text-2">
192192
{param.label}
193193
</span>
194194
<select
@@ -229,7 +229,7 @@ export function FxParamRow({
229229
>
230230
<span
231231
className={`hf-fx-label w-[86px] flex-shrink-0 truncate text-[10px] ${
232-
automated ? "text-panel-accent" : "text-panel-text-4"
232+
automated ? "text-panel-accent" : "text-panel-text-2"
233233
}`}
234234
>
235235
{param.label}
@@ -279,7 +279,7 @@ export function FxParamRow({
279279
}}
280280
/>
281281
{param.unit ? (
282-
<span className="hf-fx-unit w-[22px] flex-shrink-0 font-mono text-[9px] text-panel-text-4">
282+
<span className="hf-fx-unit w-[22px] flex-shrink-0 font-mono text-[9px] text-panel-text-2">
283283
{param.unit}
284284
</span>
285285
) : null}

packages/studio/src/components/editor/propertyPanelFxEqModule.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ function Fader({
115115
</span>
116116
<span
117117
className={`hf-fx-eq-value font-mono text-[9px] tabular-nums ${
118-
moved ? "text-panel-accent" : "text-panel-text-4"
118+
moved ? "text-panel-accent" : "text-panel-text-2"
119119
}`}
120120
>
121121
{moved ? shown(value) : "0"}
@@ -154,10 +154,10 @@ export function FxEqModule({
154154
>
155155
Tone
156156
</button>
157-
<span className="font-mono text-[9px] text-panel-text-4">{bands.length}-band</span>
157+
<span className="font-mono text-[9px] text-panel-text-2">{bands.length}-band</span>
158158
<button
159159
type="button"
160-
className="hf-fx-remove px-1 text-[11px] text-panel-text-4 hover:text-panel-danger"
160+
className="hf-fx-remove px-1 text-[11px] text-panel-text-2 hover:text-panel-danger"
161161
aria-label="Remove Tone"
162162
disabled={disabled}
163163
onClick={onRemove}
@@ -179,7 +179,7 @@ export function FxEqModule({
179179
/>
180180
))}
181181
</div>
182-
<div className="mt-1.5 flex justify-between font-mono text-[8px] tracking-wide text-panel-text-4">
182+
<div className="mt-1.5 flex justify-between font-mono text-[8px] tracking-wide text-panel-text-2">
183183
<span>CUT</span>
184184
<span>BOOST</span>
185185
</div>

packages/studio/src/components/editor/propertyPanelFxNodeRow.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ function FxMoveButton({
135135
return (
136136
<button
137137
type="button"
138-
className="hf-fx-move px-1 font-mono text-[10px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-25"
138+
className="hf-fx-move px-1 font-mono text-[10px] text-panel-text-2 hover:text-panel-text-0 disabled:opacity-25"
139139
title={label}
140140
disabled={disabled}
141141
onClick={onClick}
@@ -180,7 +180,7 @@ function FxNodeHeader({
180180
and as a list when they are not — and the difference decides whether an
181181
author thinks the order matters. It does; it is audible. */}
182182
{position !== undefined ? (
183-
<span className="hf-fx-node-index shrink-0 font-mono text-[9px] tabular-nums text-panel-text-4">
183+
<span className="hf-fx-node-index shrink-0 font-mono text-[9px] tabular-nums text-panel-text-2">
184184
{String(position).padStart(2, "0")}
185185
</span>
186186
) : null}
@@ -194,7 +194,7 @@ function FxNodeHeader({
194194
</button>
195195
<button
196196
type="button"
197-
className="hf-fx-bypass rounded-[3px] border border-panel-border-input px-1.5 py-0.5 font-mono text-[9px] text-panel-text-4 hover:text-panel-text-0 disabled:opacity-40"
197+
className="hf-fx-bypass rounded-[3px] border border-panel-border-input px-1.5 py-0.5 font-mono text-[9px] text-panel-text-2 hover:text-panel-text-0 disabled:opacity-40"
198198
aria-pressed={bypassed}
199199
title={bypassed ? "Enable" : "Bypass"}
200200
disabled={disabled}
@@ -216,7 +216,7 @@ function FxNodeHeader({
216216
/>
217217
<button
218218
type="button"
219-
className="hf-fx-remove px-1 font-mono text-[11px] text-panel-text-4 hover:text-red-400 disabled:opacity-40"
219+
className="hf-fx-remove px-1 font-mono text-[11px] text-panel-text-2 hover:text-red-400 disabled:opacity-40"
220220
title="Remove"
221221
disabled={disabled}
222222
onClick={onRemove}
@@ -377,15 +377,15 @@ export function FxNodeRow({
377377
onRemove={() => onRemove(index)}
378378
/>
379379
{summary ? (
380-
<p className="hf-fx-node-summary truncate px-1.5 pb-1 text-[10px] text-panel-text-4">
380+
<p className="hf-fx-node-summary truncate px-1.5 pb-1 text-[10px] text-panel-text-2">
381381
{summary}
382382
</p>
383383
) : null}
384384
{open ? (
385385
<>
386386
{/* What it is for, before what it is made of. */}
387387
{copy?.does ? (
388-
<p className="hf-fx-node-does border-t border-panel-border-input px-1.5 py-1 text-[10px] text-panel-text-4">
388+
<p className="hf-fx-node-does border-t border-panel-border-input px-1.5 py-1 text-[10px] text-panel-text-2">
389389
{copy.does}
390390
</p>
391391
) : null}
@@ -409,7 +409,7 @@ export function FxNodeRow({
409409
/>
410410
</div>
411411
{profile ? (
412-
<p className="hf-fx-node-ends flex justify-between gap-2 px-1.5 pb-1 text-[9px] text-panel-text-4">
412+
<p className="hf-fx-node-ends flex justify-between gap-2 px-1.5 pb-1 text-[9px] text-panel-text-2">
413413
<span className="truncate">{profile.ends.low}</span>
414414
<span className="truncate text-right">{profile.ends.high}</span>
415415
</p>
@@ -434,7 +434,7 @@ export function FxNodeRow({
434434
author where the control is; this tells them which way to move
435435
it, which is the question they actually have. */}
436436
{copy?.primaryEnds ? (
437-
<p className="hf-fx-node-ends flex justify-between gap-2 px-1.5 pb-1 text-[9px] text-panel-text-4">
437+
<p className="hf-fx-node-ends flex justify-between gap-2 px-1.5 pb-1 text-[9px] text-panel-text-2">
438438
<span className="truncate">{copy.primaryEnds.low}</span>
439439
<span className="truncate text-right">{copy.primaryEnds.high}</span>
440440
</p>
@@ -452,15 +452,15 @@ export function FxNodeRow({
452452
{oneKnob ? (
453453
<button
454454
type="button"
455-
className="hf-fx-node-details flex w-full items-center gap-1 border-t border-panel-border-input px-1.5 py-1 text-left font-mono text-[9px] uppercase tracking-wide text-panel-text-4 hover:text-panel-text-0"
455+
className="hf-fx-node-details flex w-full items-center gap-1 border-t border-panel-border-input px-1.5 py-1 text-left font-mono text-[9px] uppercase tracking-wide text-panel-text-2 hover:text-panel-text-0"
456456
aria-expanded={details}
457457
onClick={() => setDetails((was) => !was)}
458458
>
459459
<span aria-hidden="true">{details ? "\u25BE" : "\u25B8"}</span>
460460
Details — {registryDef.label}
461461
</button>
462462
) : (
463-
<p className="hf-fx-node-mechanism border-t border-panel-border-input px-1.5 pt-1 font-mono text-[9px] uppercase tracking-wide text-panel-text-4">
463+
<p className="hf-fx-node-mechanism border-t border-panel-border-input px-1.5 pt-1 font-mono text-[9px] uppercase tracking-wide text-panel-text-2">
464464
Details — {registryDef.label}
465465
</p>
466466
)}

packages/studio/src/components/editor/propertyPanelFxPresetMenu.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type HfAudioFxPresetFamily,
1414
} from "@hyperframes/core/audio-fx-presets";
1515
import { PRESET_PROBLEM } from "@hyperframes/core/audio-fx-copy";
16+
import type { HfAudioNameKind } from "@hyperframes/core/audio-carve";
1617

1718
/**
1819
* Shelf names in the author's language, which is deliberately not the effect
@@ -28,6 +29,17 @@ const FAMILY_LABEL: Record<HfAudioFxPresetFamily, string> = {
2829
};
2930

3031
export interface FxPresetMenuProps {
32+
/**
33+
* What the track reads as, from its id and filename.
34+
*
35+
* Only a confident "music" or "sfx" hides anything. `unknown` keeps the whole
36+
* shelf, on the same principle the carve's source picker follows: a name is a
37+
* hint, and a shelf that hides what somebody came for is worse than a long
38+
* one. Nothing here is irreversible either — the cost of a wrong guess is one
39+
* shelf the author has to scroll past, against the cost of "My voice sounds
40+
* amateur" sitting on a music bed.
41+
*/
42+
trackKind?: HfAudioNameKind;
3143
onPick(id: string): void;
3244
/**
3345
* Play this preset on the running audio without persisting it, and revert on
@@ -46,7 +58,15 @@ export interface FxPresetMenuProps {
4658
* in a column is a wall, and they are already the author's grouping rather than
4759
* the registry's. See `plans/audio-fx-ux/README.md` §Decided.
4860
*/
49-
export function FxPresetMenu({ onPick, onAudition }: FxPresetMenuProps) {
61+
export function FxPresetMenu({ trackKind, onPick, onAudition }: FxPresetMenuProps) {
62+
// The voice presets all begin by cutting rumble out of a human voice and end
63+
// in a compressor set for speech. On a music bed that is not a mild mismatch,
64+
// it is the wrong instrument — and the shelf leads with the complaint, so it
65+
// would be offering the author a problem they do not have.
66+
const families =
67+
trackKind === "music" || trackKind === "sfx"
68+
? HF_AUDIO_FX_PRESET_FAMILIES.filter((f) => f !== "voice")
69+
: HF_AUDIO_FX_PRESET_FAMILIES;
5070
return (
5171
<div
5272
className="hf-fx-preset-menu space-y-1.5 rounded-[4px] border border-panel-border-input p-1.5"
@@ -59,9 +79,9 @@ export function FxPresetMenu({ onPick, onAudition }: FxPresetMenuProps) {
5979
// button's focus, so it reverts and re-auditions rather than sticking.
6080
onBlur={onAudition ? () => onAudition(null) : undefined}
6181
>
62-
{HF_AUDIO_FX_PRESET_FAMILIES.map((family) => (
82+
{families.map((family) => (
6383
<div key={family} className="hf-fx-preset-group space-y-0.5">
64-
<span className="hf-fx-preset-group-label block font-mono text-[9px] uppercase tracking-wide text-panel-text-4">
84+
<span className="hf-fx-preset-group-label block font-mono text-[9px] uppercase tracking-wide text-panel-text-2">
6585
{FAMILY_LABEL[family]}
6686
</span>
6787
{audioFxPresetsByFamily(family).map((preset) => (
@@ -85,7 +105,7 @@ export function FxPresetMenu({ onPick, onAudition }: FxPresetMenuProps) {
85105
<span className="hf-fx-preset-problem block truncate text-[10px]">
86106
{PRESET_PROBLEM[preset.id] ?? preset.description}
87107
</span>
88-
<span className="hf-fx-preset-name block truncate font-mono text-[9px] text-panel-text-4">
108+
<span className="hf-fx-preset-name block truncate font-mono text-[9px] text-panel-text-2">
89109
{preset.label}
90110
</span>
91111
{/* Hovering a preset plays it, and playing is otherwise invisible:

0 commit comments

Comments
 (0)