diff --git a/packages/studio/src/player/components/Timeline.test.ts b/packages/studio/src/player/components/Timeline.test.ts index 46df3319e..550a9b1ed 100644 --- a/packages/studio/src/player/components/Timeline.test.ts +++ b/packages/studio/src/player/components/Timeline.test.ts @@ -433,7 +433,10 @@ describe("Timeline provider boundary", () => { button.click(); }); - const row = button.parentElement?.parentElement; + // Up from the rowheader rather than counting parents: the header now lays + // its name and its controls out on two lines, so the button sits one level + // deeper than it used to. + const row = button.closest('[role="rowheader"]')?.parentElement; // Row children: [TimelineTrackHeader (sticky column), time-mapped content]. const trackContent = row?.children.item(1); expect(onToggleTrackHidden).toHaveBeenCalledWith(0, false); diff --git a/packages/studio/src/player/components/TimelineGroupHeader.tsx b/packages/studio/src/player/components/TimelineGroupHeader.tsx index 6fca6a656..b10f84963 100644 --- a/packages/studio/src/player/components/TimelineGroupHeader.tsx +++ b/packages/studio/src/player/components/TimelineGroupHeader.tsx @@ -190,20 +190,28 @@ export function TimelineGroupHeader({ aria-pressed={isSoloed} aria-label="Hear only this" 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] ${ - isSoloed - ? "text-[#F5C542] hover:text-white" - : isHalfLitSolo - ? "text-[#F5C542]/50 hover:text-white" - : "text-white/35 hover:text-white/75" - }`} + className="flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC]" onPointerDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onToggleSolo({ add: event.metaKey || event.ctrlKey }); }} > - + {/* Three states, not two: filled when this group is soloed, and + HALF-lit when a member is — the affordance for "this bus is + passing audio, but I did not solo it" (groups doc §2.2). */} + event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onToggle({ add: event.metaKey || event.ctrlKey }); }} > - + {/* Filled when on, outlined when off — the state has to read at a glance + from across the track column, and a colour change alone does not. */} + ); } diff --git a/packages/studio/src/player/components/TimelineTrackHeader.tsx b/packages/studio/src/player/components/TimelineTrackHeader.tsx index 9c88b7cc9..dba23258f 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.tsx @@ -493,7 +493,7 @@ export function TimelineTrackHeader({ className={`sticky left-0 z-[12] shrink-0 ${ !isKeyframeLayer ? showTrackLabel - ? "flex items-center gap-1 px-1.5 text-white/55" + ? "flex flex-col justify-center gap-0.5 px-1.5 text-white/55" : "flex flex-col items-center justify-center gap-0.5" : "" }`} @@ -529,25 +529,30 @@ export function TimelineTrackHeader({ isSoloed={soloTargetId !== null && soloed.has(soloTargetId)} onToggleSolo={soloTargetId ? (options) => toggleSolo(soloTargetId, options) : undefined} onToggleTrackHidden={onToggleTrackHidden} + // On the control line, beside mute and solo — not a third row. + trailing={ + <> + {singleAudioClip && isCanaryEnabled("audio-fx-rack") && ( + writeClipFxChain(singleAudioClip, next, false)} + onChainPreview={(next) => writeClipFxChain(singleAudioClip, next, true)} + // Muted, an audition is silent — so the hover lifts the mute on + // the running graph and puts it back on the way out, the same + // borrow-and-return it already does with the playhead. + auditionSpans={[singleAudioClip]} + isMuted={isTrackHidden} + onSetMutedLive={(muted) => + onSetElementAttributeLive?.(singleAudioClip, "data-hidden", muted ? "" : null) + } + onOpenRack={() => openClipFxRack(singleAudioClip)} + /> + )} + + } /> - {singleAudioClip && isCanaryEnabled("audio-fx-rack") && ( - writeClipFxChain(singleAudioClip, next, false)} - onChainPreview={(next) => writeClipFxChain(singleAudioClip, next, true)} - // Muted, an audition is silent — so the hover lifts the mute on - // the running graph and puts it back on the way out, the same - // borrow-and-return it already does with the playhead. - auditionSpans={[singleAudioClip]} - isMuted={isTrackHidden} - onSetMutedLive={(muted) => - onSetElementAttributeLive?.(singleAudioClip, "data-hidden", muted ? "" : null) - } - 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. */} diff --git a/packages/studio/src/player/components/TimelineTrackPlainHeader.tsx b/packages/studio/src/player/components/TimelineTrackPlainHeader.tsx index e5dacfcd9..19a404569 100644 --- a/packages/studio/src/player/components/TimelineTrackPlainHeader.tsx +++ b/packages/studio/src/player/components/TimelineTrackPlainHeader.tsx @@ -1,3 +1,4 @@ +import type React from "react"; import { Eye, EyeSlash, SpeakerHigh, SpeakerSlash } from "@phosphor-icons/react"; import { isCanaryEnabled } from "../../telemetry/canary"; import { Music } from "../../icons/SystemIcons"; @@ -72,6 +73,7 @@ export function PlainTrackHeader({ isSoloed, onToggleSolo, onToggleTrackHidden, + trailing, }: { trackNumber: number; trackDisplayNumber: number | null; @@ -84,41 +86,52 @@ export function PlainTrackHeader({ isGroupMuted: boolean; isSoloed: boolean; onToggleSolo?: (options?: { add?: boolean }) => void; + /** Trailing controls that belong on the control line — the FX entry points, + * which the caller owns because only it knows the clip they act on. */ + trailing?: React.ReactNode; }) { return ( <> - {isAudioTrack && ( -