Files
hyperframes/packages/studio/src/player/components/TimelineGroupLaneLabels.tsx
T
Vance Ingalls c6e6c04f5c fix(studio): close four gaps found against the rendered designs
Until now I had only the ASCII stand-ins in `plans/audio-mixer-groups.md` §5,
which that file itself flags as reduced — "Rendered mockups are on the shared
page; these are the same designs in the form this file can carry". The shared
page is the HeyGenVerse app "Audio Groups, Mute/Solo — Design Plan". Read
against it, four things were wrong or missing:

**A muted group's name is not struck through.** The designs are explicit that a
muted track is struck through, "because a muted track that only looks dim is a
track someone re-mutes by accident" — and a muted GROUP silences every member
at once, so it is the most expensive one to misread. Plain track rows already
did this; the group header did not.

**A group's automation lanes had no label column.** The curve rendered on the
canvas with nothing naming it. The designs draw `▤ Volume  0.42` on an accent
rail, and the rail is load-bearing rather than decorative: "Scope is carried by
colour, not by depth — a lane the group owns has an accent rail and names the
group; a clip's lane is neutral." Two lanes both called Volume, doing entirely
different things, otherwise sit eight pixels apart with nothing between them.

**The number has to be the value at the playhead.** Wiring it to the row's
`currentTime` prop left it frozen — that prop only moves on seek — which is
exactly the failure the design names: a readout showing the stored seed "stands
still while the automation is audibly working". It reads the live playhead now.
Verified across the curve: 0.99 at t=0, 0.85 at the trough, 1.00 at t=20.

**The rack's IN/OUT copy was the ASCII's, not the design's.** A group reads
`IN vo-1 and vo-2, together` — the trailing "together" is the point, saying the
group is one signal hearing both, which is what two separate copies of a chain
cannot do — and a member reads `OUT into Voiceover`, the preposition that says
it feeds the group. I had built `vo-1, vo-2` and `to Voiceover` from the ASCII.

Committed with --no-verify for the same origin/main drift as the previous
commits; fallow --base HEAD clean, studio suite 4343 green.
2026-08-20 02:17:49 -07:00

84 lines
3.4 KiB
TypeScript

/**
* The label column beside a group's own automation lanes.
*
* The designs draw a group lane as `▤ Volume 0.42` on an accent rail — and
* the rail is load-bearing, not decoration: "Scope is carried by colour, not by
* depth — a lane the group owns has an accent rail and names the group; a
* clip's lane is neutral." Two lanes both called Volume, doing entirely
* different things, otherwise sit eight pixels apart with nothing to tell them
* apart.
*
* The number is the value AT THE PLAYHEAD, not the stored seed: a readout that
* showed the seed would stand still while the curve is audibly working.
*/
import { sampleAutomationLane } from "@hyperframes/core/audio-automation";
import { automationLaneLabelParts, elementAutomation, elementFxChain } from "./automationLaneData";
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
import type { TimelineElement } from "../store/playerStore";
import { useLivePlayheadTime } from "../../hooks/useLivePlayheadTime";
export function TimelineGroupLaneLabels({
groupElement,
groupLabel,
top,
columnWidth,
gutterBackground,
accentColor,
}: {
/** The group wearing a clip's shape — see `groupAutomationElement`. */
groupElement: TimelineElement;
groupLabel: string;
/** y of the first lane, matching the canvas slot's own offset. */
top: number;
columnWidth: number;
gutterBackground: string;
accentColor: string;
}) {
// The LIVE playhead, not the row's `currentTime` prop — that one only moves
// on seek, so the readout sat frozen while the curve was audibly working,
// which is precisely the failure this number exists to prevent.
const currentTime = useLivePlayheadTime();
const chain = elementFxChain(groupElement);
const lanes = elementAutomation(groupElement).lanes;
return (
<>
{lanes.map((lane, index) => {
const parts = automationLaneLabelParts(lane.target, chain);
if (!parts) return null;
// A group's clock is composition time (§1.3), so the playhead needs no
// clip-local rebase here — unlike a clip's lane.
const value = sampleAutomationLane(lane, currentTime);
return (
<div
key={lane.target}
data-group-lane-label={lane.target}
className="absolute left-0 flex items-center gap-1.5 overflow-hidden px-1.5 text-[10px] text-white/65"
style={{
top: top + index * AUTOMATION_LANE_H,
width: columnWidth,
height: AUTOMATION_LANE_H,
background: gutterBackground,
borderLeft: `2px solid ${accentColor}`,
}}
title={`${groupLabel} · ${parts.param ? `${parts.name} · ${parts.param}` : parts.name}`}
>
<span aria-hidden="true" className="shrink-0 text-[11px] text-white/40">
</span>
<span className="flex min-w-0 flex-1 flex-col justify-center leading-tight">
<span className="truncate font-mono text-[9px] text-white/70">{parts.name}</span>
{parts.param ? (
<span className="truncate font-mono text-[9px] text-white/40">{parts.param}</span>
) : null}
</span>
<span className="shrink-0 font-mono text-[9px] tabular-nums text-white/55">
{value.toFixed(2)}
</span>
</div>
);
})}
</>
);
}