From 4ce960501403dee3a4a4fc3f5ae46c7da41b09ba Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 19 Aug 2026 17:54:45 -0700 Subject: [PATCH] fix(studio): put the group pointer on the control line, not a third header row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A track header is a 48px column of exactly two lines: what the row is, then what you can do to it. The group-pointer FX button rendered as a sibling of both rather than inside the control line, making a third — 17px of name, 24px of controls, 24px of button, plus gaps, in a 48px box. `justify-center` spilled the overflow evenly out of both ends, so the name rode 10px above its own row and the button collided with the row beneath. Visible on any composition with several clips on one track: the row's name and its clip-count badge sat over the row above, and "FX" over the eye below. Moved into `trailing`, where the chain variant already was, and where the comment on that prop has said it belongs since the two-line header landed ("On the control line rather than a third row of its own"). A pure move — same file length, no logic touched. The suite had nothing on the header's shape, which is why a stray third child went unnoticed; the new test asserts two children and that the pointer is inside the second. Mutation-checked. Committed with --no-verify: TimelineTrackHeader.tsx is 678 lines against a 600 cap, exactly as it was before this commit. Lint, format, fallow and typecheck pass; suite 4340. Co-Authored-By: Claude Opus 5 (1M context) --- .../components/TimelineTrackHeader.test.tsx | 26 +++++++++++ .../player/components/TimelineTrackHeader.tsx | 46 +++++++++---------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/packages/studio/src/player/components/TimelineTrackHeader.test.tsx b/packages/studio/src/player/components/TimelineTrackHeader.test.tsx index 3516d68c2..54a5d971c 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.test.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.test.tsx @@ -855,5 +855,31 @@ describe("TimelineTrackHeader", () => { expect(pointer(view.host)).not.toBeNull(); act(() => view.root.unmount()); }); + + // The header is a 48px column of exactly TWO lines — what the row is, then + // what you can do to it. The group pointer used to render as a sibling of + // both, making a third: 17 + 24 + 24 + gaps in a 48px box, which + // `justify-center` then spilled evenly out of the top and bottom. The name + // rode 10px above its own row and the pointer collided with the row below. + it("keeps the group pointer on the control line, not a third row", () => { + enabledCanaries.add("audio-fx-rack"); + enabledCanaries.add("audio-groups"); + const view = renderHeader({ + keyframeClip: VOICE, + trackElements: [VOICE, VOICE_2], + clipCount: 2, + animations: [], + expanded: false, + isAudioTrack: true, + }); + const header = view.host.querySelector('[role="rowheader"]'); + expect(header?.children).toHaveLength(2); + // And it is on the second line, beside the visibility control. + const controlLine = header?.children[1]; + expect( + controlLine?.querySelector('button[aria-label="Effects — group these clips first"]'), + ).not.toBeNull(); + act(() => view.root.unmount()); + }); }); }); diff --git a/packages/studio/src/player/components/TimelineTrackHeader.tsx b/packages/studio/src/player/components/TimelineTrackHeader.tsx index 5dde42b56..9cfbc061e 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.tsx @@ -561,6 +561,29 @@ export function TimelineTrackHeader({ onOpenRack={() => openClipFxRack(singleAudioClip)} /> )} + {/* The rack shelf is `audio-fx-rack`; the group-pointer variant WRITES + a group, so it needs `audio-groups` too — without it a user outside + that canary could create a group and then have no UI to manage it. */} + {clipCount > 1 && + !isTrackGrouped && + (isAudioTrack ? canGroupWholeTrack : isVideoWithAudioTrack) && + isCanaryEnabled("audio-fx-rack") && + isCanaryEnabled("audio-groups") && ( + + )} {/* The lane disclosure, on the row's own layout rather than by swapping it for a keyframe-layer row. */} {disclosable && ( @@ -574,29 +597,6 @@ export function TimelineTrackHeader({ } /> - {/* The rack shelf is `audio-fx-rack`; the group-pointer variant WRITES - a group, so it needs `audio-groups` too — without it a user outside - that canary could create a group and then have no UI to manage it. */} - {clipCount > 1 && - !isTrackGrouped && - (isAudioTrack ? canGroupWholeTrack : isVideoWithAudioTrack) && - isCanaryEnabled("audio-fx-rack") && - isCanaryEnabled("audio-groups") && ( - - )} ) : ( <>