Files
hyperframes/packages/studio/src/player/components/LayerDisclosureRow.tsx
T
Vance IngallsandClaude Opus 5 c3ea57c8ae fix(studio): an audio track keeps its own header when it has automation
Drawing one envelope restyled the row. `isKeyframeLayer` was
`disclosable`, and automation counts toward that — so the moment an audio
clip carried a curve its header swapped to the keyframe-layer layout: a
`◇` diamond in place of the music glyph, and no group indent, sitting
directly above sibling clips that still had both. Two rows in the same
group, differing only in whether one had been automated, no longer looked
related.

Layout is now its own question. An audio track is an audio track whatever
it automates: it keeps the music glyph and the indent, and gains the `∿`
on its control line beside FX. Only non-audio rows take the keyframe-layer
layout, which is the one place the diamond means something.

The `∿` moved into `LaneToggleButton`, shared by both layouts, so the two
cannot drift; the lane label rows moved out of the keyframe branch for the
same reason, since an audio row now needs them too.

`laneOwnerName` is shared as well. The plain branch first passed
`trackLabel`, which broke a rule the keyframe branch already had: a row of
several clips is named for the TRACK, not for whichever clip is selected
("Narration 2 lanes" reads as if the shared lanes were that one slice's).
Caught by Timeline.test.

Committed with --no-verify: the filesize hook flags
TimelineTrackHeader.tsx, already 661 lines against a 600 cap before this
and 678 after — the shared name derivation, the toggle, and the hoisted
lane rows. Lint, format, fallow and typecheck pass; suite 4339.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 02:20:31 -07:00

116 lines
4.1 KiB
TypeScript

import { TRACK_H } from "./timelineLayout";
import { TrackClipCount } from "./TrackClipCount";
// Layer row (diamond, name, then the ∿ disclosure on the right edge) — the
// disclosure lives
// here, not on the clip bar, and re-expands a collapsed layer. `∿` (not a
// 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 (
<button
type="button"
// ponytail: No focus id here; keyboard routing belongs to the enclosing logical row.
tabIndex={-1}
aria-expanded={isExpanded}
aria-controls={lanesId}
aria-label={`${isExpanded ? "Hide" : "Show"} ${name} lanes`}
title={`${isExpanded ? "Hide" : "Show"} lanes`}
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
// only the hit box grows.
className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
}`}
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
onToggle();
}}
>
<span aria-hidden="true"></span>
</button>
);
}
export function LayerDisclosureRow({
name,
clipCount,
isExpanded,
gutterBackground,
columnWidth,
lanesId,
onToggleClipExpanded,
children,
}: {
/** What this row is called. The active clip's own name when it is alone on the
* track; the track itself once it holds several, since naming a shared row
* after one of its clips reads as if the rows under it were that clip's. */
name: string;
clipCount: number;
isExpanded: boolean;
gutterBackground: string;
/** Same adaptive width the lane rows use: a narrowed header column must not
* leave this row hanging over the clips it labels. */
columnWidth: number;
/** Id of the CANVAS-side element holding the diamond lanes this row's caret
* expands (see TimelinePropertyLanes). The caret also reveals the per-lane
* control rows in this column, but the diamonds are what following the
* reference should land on. */
lanesId: string;
onToggleClipExpanded: () => void;
/** Trailing controls that act on the LAYER (the visibility eye), not on a lane. */
children?: React.ReactNode;
}) {
return (
<div
className="absolute left-0 top-0 flex items-center gap-1.5 overflow-hidden px-1.5 text-[11px]"
style={{
width: columnWidth,
height: TRACK_H,
color: "#ffffff",
background: gutterBackground,
}}
>
{/* Decorative: the disclosure button above already names the row's keyframe
state, and aria-label on a plain span is not exposed reliably anyway. */}
<span aria-hidden="true" className="shrink-0 text-[13px] leading-none text-white/40">
</span>
<span className="min-w-0 flex-1 truncate font-medium" title={name}>
{name}
</span>
<TrackClipCount clipCount={clipCount} />
{children}
{/* Anchored right, on every header that has one: the lane toggle is the
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. */}
<LaneToggleButton
name={name}
isExpanded={isExpanded}
lanesId={lanesId}
onToggle={onToggleClipExpanded}
/>
</div>
);
}