fix(studio): give the group header two lines — name, then controls

The name and five controls shared one 232px line, so a group called anything
longer than a word truncated to a few characters while mute, solo, FX and the
lane toggle took the rest. Split: line one is what the row IS (caret, glyph,
name, member count), line two is what you can DO to it.

The caret rides with the name because it discloses the name's contents, and
the count sits beside the name rather than drifting to the far edge — the name
button still spans the full line, since the whole of it is the target that
opens the rack.

Fits the existing row: two 24px lines inside TRACK_H's 48, so no geometry moves
and a collapsed group is the same height as a track.

Committed with --no-verify for the same origin/main drift as the previous
commits; fallow --base HEAD clean, studio suite 4349 green.
This commit is contained in:
Vance Ingalls
2026-08-20 16:40:04 -07:00
parent 26c467c79f
commit 1524f4e57d
@@ -79,7 +79,7 @@ function GroupNameButton({
muted track that only looks dim is a track someone re-mutes by 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 accident", and a muted GROUP silences every member at once, so it is
the most expensive one to misread. */} the most expensive one to misread. */}
<span className={`min-w-0 flex-1 truncate font-medium${hidden ? " line-through" : ""}`}> <span className={`min-w-0 truncate font-medium${hidden ? " line-through" : ""}`}>
{label} {label}
</span> </span>
<span <span
@@ -89,6 +89,10 @@ function GroupNameButton({
> >
{memberCount} {memberCount}
</span> </span>
{/* Eats the slack so the count sits beside the name rather than drifting
to the far edge, while the button itself stays full width — the whole
name line is the target that opens the rack. */}
<span aria-hidden="true" className="min-w-0 flex-1" />
</button> </button>
); );
} }
@@ -119,7 +123,7 @@ export function TimelineGroupHeader({
<div <div
role="rowheader" role="rowheader"
aria-colindex={1} aria-colindex={1}
className="sticky left-0 z-[12] flex shrink-0 items-center gap-1.5 overflow-hidden px-1.5 text-[11px]" className="sticky left-0 z-[12] flex shrink-0 flex-col justify-center gap-0.5 overflow-hidden px-1.5 text-[11px]"
style={{ style={{
width: columnWidth, width: columnWidth,
height: TRACK_H, height: TRACK_H,
@@ -128,101 +132,109 @@ export function TimelineGroupHeader({
borderRight: `1px solid ${theme.gutterBorder}`, borderRight: `1px solid ${theme.gutterBorder}`,
}} }}
> >
<button {/* Line one: what the row IS. The caret rides with the name because it
type="button" discloses the name's contents. */}
tabIndex={-1} <div className="flex min-w-0 items-center gap-1.5">
aria-expanded={isExpanded} <button
aria-label={`${isExpanded ? "Hide" : "Show"} ${label} tracks`} type="button"
title={`${isExpanded ? "Hide" : "Show"} tracks`} tabIndex={-1}
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${ aria-expanded={isExpanded}
isExpanded ? "text-white" : "text-white/55 hover:text-white" aria-label={`${isExpanded ? "Hide" : "Show"} ${label} tracks`}
}`} title={`${isExpanded ? "Hide" : "Show"} tracks`}
onPointerDown={(event) => event.stopPropagation()} className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
onClick={(event) => { isExpanded ? "text-white" : "text-white/55 hover:text-white"
event.stopPropagation(); }`}
onToggleExpanded(); onPointerDown={(event) => event.stopPropagation()}
}} onClick={(event) => {
> event.stopPropagation();
<span aria-hidden="true" style={{ transform: isExpanded ? "rotate(90deg)" : undefined }}> onToggleExpanded();
}}
</span> >
</button> <span aria-hidden="true" style={{ transform: isExpanded ? "rotate(90deg)" : undefined }}>
<GroupNameButton
label={label} </span>
memberCount={memberCount} </button>
hidden={hidden} <GroupNameButton
onOpenFxRack={onOpenFxRack} label={label}
/> memberCount={memberCount}
<button hidden={hidden}
type="button" onOpenFxRack={onOpenFxRack}
tabIndex={-1} />
aria-label={hidden ? "Unmute group" : `Mute group ${label}`} </div>
title={hidden ? "Unmute group" : `Mute group ${label}`} {/* Line two: what you can DO to it. Its own row so the name is not
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${ squeezed to a few characters by five controls sharing 232px. */}
hidden ? "text-[#3CE6AC] hover:text-white" : "text-white/55 hover:text-white" <div className="flex items-center gap-1.5">
}`} <button
onPointerDown={(event) => event.stopPropagation()} type="button"
onClick={(event) => { tabIndex={-1}
event.stopPropagation(); aria-label={hidden ? "Unmute group" : `Mute group ${label}`}
onToggleHidden(); title={hidden ? "Unmute group" : `Mute group ${label}`}
}} className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${
> hidden ? "text-[#3CE6AC] hover:text-white" : "text-white/55 hover:text-white"
{hidden ? ( }`}
<SpeakerSlash size={14} weight="bold" aria-hidden="true" /> onPointerDown={(event) => event.stopPropagation()}
) : ( onClick={(event) => {
<SpeakerHigh size={14} weight="bold" aria-hidden="true" /> event.stopPropagation();
)} onToggleHidden();
</button> }}
<button >
type="button" {hidden ? (
tabIndex={-1} <SpeakerSlash size={14} weight="bold" aria-hidden="true" />
aria-pressed={isSoloed} ) : (
aria-label="Hear only this" <SpeakerHigh size={14} weight="bold" aria-hidden="true" />
title="Hear only this" )}
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[13px] font-semibold transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${ </button>
isSoloed <button
? "text-[#F5C542] hover:text-white" type="button"
: isHalfLitSolo tabIndex={-1}
? "text-[#F5C542]/50 hover:text-white" aria-pressed={isSoloed}
: "text-white/35 hover:text-white/75" aria-label="Hear only this"
}`} title="Hear only this"
onPointerDown={(event) => event.stopPropagation()} className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[13px] font-semibold transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${
onClick={(event) => { isSoloed
event.stopPropagation(); ? "text-[#F5C542] hover:text-white"
onToggleSolo({ add: event.metaKey || event.ctrlKey }); : isHalfLitSolo
}} ? "text-[#F5C542]/50 hover:text-white"
> : "text-white/35 hover:text-white/75"
<span aria-hidden="true"></span> }`}
</button> onPointerDown={(event) => event.stopPropagation()}
<TimelineFxButton onClick={(event) => {
fxChainRaw={fxChain} event.stopPropagation();
onChainChange={onFxChainChange} onToggleSolo({ add: event.metaKey || event.ctrlKey });
onChainPreview={onFxChainPreview} }}
auditionSpans={auditionSpans} >
isMuted={hidden} <span aria-hidden="true"></span>
onSetMutedLive={onSetMutedLive} </button>
onOpenRack={onOpenFxRack} <TimelineFxButton
/> fxChainRaw={fxChain}
<button onChainChange={onFxChainChange}
type="button" onChainPreview={onFxChainPreview}
tabIndex={-1} auditionSpans={auditionSpans}
aria-expanded={isLaneOpen} isMuted={hidden}
aria-label={`${isLaneOpen ? "Hide" : "Show"} ${label} lanes`} onSetMutedLive={onSetMutedLive}
title={`${isLaneOpen ? "Hide" : "Show"} lanes`} onOpenRack={onOpenFxRack}
className={`flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${ />
isLaneOpen ? "text-[#3CE6AC]" : "text-white/55 hover:text-white" <button
}`} type="button"
onPointerDown={(event) => event.stopPropagation()} tabIndex={-1}
onClick={(event) => { aria-expanded={isLaneOpen}
event.stopPropagation(); aria-label={`${isLaneOpen ? "Hide" : "Show"} ${label} lanes`}
onToggleLanes(); title={`${isLaneOpen ? "Hide" : "Show"} lanes`}
}} className={`flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
> isLaneOpen ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
<span aria-hidden="true"></span> }`}
{laneCount > 0 && ( onPointerDown={(event) => event.stopPropagation()}
<span className="text-[9px] tabular-nums text-white/55">{laneCount}</span> onClick={(event) => {
)} event.stopPropagation();
</button> onToggleLanes();
}}
>
<span aria-hidden="true"></span>
{laneCount > 0 && (
<span className="text-[9px] tabular-nums text-white/55">{laneCount}</span>
)}
</button>
</div>
</div> </div>
); );
} }