diff --git a/packages/studio/src/player/components/LayerDisclosureRow.tsx b/packages/studio/src/player/components/LayerDisclosureRow.tsx index 718b8e127..6ce5e339b 100644 --- a/packages/studio/src/player/components/LayerDisclosureRow.tsx +++ b/packages/studio/src/player/components/LayerDisclosureRow.tsx @@ -7,6 +7,50 @@ import { TrackClipCount } from "./TrackClipCount"; // caret) because a group's own row keeps the caret for its structural // disclosure (member rows) — this button only ever means "show this row's // lanes", so it needs its own distinct glyph. +/** + * The `∿` that shows or hides a row's lanes. + * + * Shared, because two layouts need the identical control: the keyframe layer + * row below, and the plain track header — an audio track with automation keeps + * its own look (music glyph, indent) and gains this, rather than being + * re-rendered as a keyframe layer to get at the button. + */ +export function LaneToggleButton({ + name, + isExpanded, + lanesId, + onToggle, +}: { + name: string; + isExpanded: boolean; + lanesId: string; + onToggle: () => void; +}) { + return ( + + ); +} + export function LayerDisclosureRow({ name, clipCount, @@ -60,27 +104,12 @@ export function LayerDisclosureRow({ row's last word about itself, and a left-hand ∿ put it where the eye looks for identity instead. `ml-auto` rather than a spacer so it holds the edge whatever else the row grows. */} - + ); } diff --git a/packages/studio/src/player/components/TimelineTrackHeader.tsx b/packages/studio/src/player/components/TimelineTrackHeader.tsx index ea5714e47..5dde42b56 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.tsx @@ -18,7 +18,7 @@ import { getTimelinePropertyLanes } from "./TimelinePropertyLanes"; import { groupAutomationLanes } from "./automationLaneData"; import { AUTOMATION_LANE_H } from "./automationLaneHeight"; import { clipTimingStart } from "../../hooks/gsapShared"; -import { LayerDisclosureRow } from "./LayerDisclosureRow"; +import { LaneToggleButton, LayerDisclosureRow } from "./LayerDisclosureRow"; import { LABEL_COL_W, LANE_H, getTimelineLaneTop } from "./timelineLayout"; import type { TimelineTheme } from "./timelineTheme"; import { @@ -444,7 +444,20 @@ export function TimelineTrackHeader({ // Automation counts as something to disclose: gating the caret on tweens alone // left an audio clip's envelopes unreachable, since the track could not expand. const disclosable = lanes.length > 0 || automationRows.length > 0; - const isKeyframeLayer = !!keyframeClip && disclosable; + // Which HEADER LAYOUT the row wears — not the same question as `disclosable`. + // An audio track that automates something is still an audio track: it keeps + // the music glyph and the group indent and gains the `∿`. Tying layout to + // disclosability swapped it for the keyframe-layer row (a `◇`, no indent) the + // moment an envelope appeared. + const isKeyframeLayer = !!keyframeClip && disclosable && !isAudioTrack; + // What the lane disclosure calls this row. A row of several clips is named + // for the TRACK, not for whichever is selected — the lanes are the track's, + // shared per property, so "Narration 2 lanes" read as if they were that one + // slice's. Shared by both layouts so the name cannot change with the layout. + const laneOwnerName = + clipCount > 1 + ? `Track${trackDisplaySuffix(trackDisplayNumber)}` + : (keyframeClip?.label ?? keyframeClip?.domId ?? keyframeClip?.id ?? trackLabel); // C1: the FX entry point. A single audio clip has one chain to point at; a // track holding several ungrouped ones has no single chain — the design @@ -516,7 +529,7 @@ export function TimelineTrackHeader({ : {}), }} > - {!keyframeClip || !disclosable ? ( + {!isKeyframeLayer ? ( <> openClipFxRack(singleAudioClip)} /> )} + {/* The lane disclosure, on the row's own layout rather than by + swapping it for a keyframe-layer row. */} + {disclosable && ( + + )} } /> @@ -578,15 +601,7 @@ export function TimelineTrackHeader({ ) : ( <> 1 - ? `Track${trackDisplaySuffix(trackDisplayNumber)}` - : (keyframeClip.label ?? keyframeClip.domId ?? keyframeClip.id) - } + name={laneOwnerName} clipCount={clipCount} isExpanded={isExpanded} gutterBackground={gutterFill(theme.gutterBackground, isGroupMember)} @@ -608,55 +623,56 @@ export function TimelineTrackHeader({ onToggle={onToggleTrackHidden} /> - {/* The caret expands TWO disjoint subtrees: these label-column rows, - which carry the per-lane keyframe controls, and the diamond lanes - on the canvas. `lanesId` names the canvas lanes (rendered by - TimelineLanes), because that is what a sighted user watches appear - and what following the reference has to land on. These rows are not - empty and are not the target; they are absolutely positioned inside - the sticky column, which is what made a wrapper HERE compute to - 0x0 and hold no diamonds. */} - {isExpanded && - lanes.map((lane, laneIndex) => ( - - ))} - {/* Below the keyframe rows and stepping by its own height, which is how - TimelineAutomationLaneSlot lays the envelopes out on the canvas. The - two have to agree or a name labels the wrong curve. */} - {isExpanded && - automationRows.map((row, index) => ( - - ))} )} + {/* The caret expands TWO disjoint subtrees: these label-column rows, + which carry the per-lane keyframe controls, and the diamond lanes + on the canvas. `lanesId` names the canvas lanes (rendered by + TimelineLanes), because that is what a sighted user watches appear + and what following the reference has to land on. These rows are not + empty and are not the target; they are absolutely positioned inside + the sticky column, which is what made a wrapper HERE compute to + 0x0 and hold no diamonds. */} + {isExpanded && + keyframeClip && + lanes.map((lane, laneIndex) => ( + + ))} + {/* Below the keyframe rows and stepping by its own height, which is how + TimelineAutomationLaneSlot lays the envelopes out on the canvas. The + two have to agree or a name labels the wrong curve. */} + {isExpanded && + automationRows.map((row, index) => ( + + ))} ); }