Files
hyperframes/packages/studio/src/player/components/timelineTrackDisplay.ts
T
Miguel Angel Simon Sierra ba0d6406d2 fix(studio): scope lane ids per timeline and stop inventing a track row
Two latent defects in the announcement path this branch adds.

- lanesId was keyed by render row alone, so a second TimelineLanes on the
  page (a mini-timeline beside the main one) would mint the same
  timeline-lanes-track-0 and every caret's aria-controls would resolve to
  whichever instance mounted first. The prefix now comes from useId, with the
  colons stripped so the id stays a legal CSS selector.
- trackDisplayNumber returned trackOrder.length + 1 for a key it could not
  find, which is indistinguishable from a real row: the label announced a row
  the user can see is wrong and nothing upstream could tell it had guessed. It
  returns null now, and trackDisplaySuffix drops the number from the label
  rather than inventing one.
2026-07-28 21:21:27 +02:00

39 lines
1.7 KiB
TypeScript

/**
* The one owner of "what track number does the user see".
*
* `TimelineElement.track` is a z-order SORT key, not a row number: an expanded
* sub-composition child gets a synthesized fractional key (`host.track + n /
* (siblings + 2)`, see `useExpandedTimelineElements`), so putting it in a string
* announces "Hide track 0.16666666666666666". Every user-visible track number,
* whether it is rendered by a component or baked into an undo-history label,
* routes through here; the raw key stays in callbacks and lookups only.
*/
/** Ascending distinct track keys, the row order the timeline renders in. */
export function timelineTrackOrder(elements: readonly { track: number }[]): number[] {
return [...new Set(elements.map((element) => element.track))].sort((a, b) => a - b);
}
/**
* A track key's 1-based display row, or null when the key is not in
* `trackOrder` at all.
*
* Both callers build `trackOrder` from the same elements the key came from, so
* null is unreachable by construction today. It is null rather than a number
* because the only numbers available to return (the end row, the last row) are
* indistinguishable from a real answer: a label would announce a row the user
* can see is wrong, and nothing upstream would ever learn it had guessed.
*/
export function trackDisplayNumber(trackOrder: readonly number[], track: number): number | null {
const row = trackOrder.indexOf(track);
return row < 0 ? null : row + 1;
}
/**
* The `" 3"` in `Hide track 3`, empty when there is no display row to name.
* Announcing "Hide track" is thin; announcing an invented row is wrong.
*/
export function trackDisplaySuffix(displayNumber: number | null): string {
return displayNumber === null ? "" : ` ${displayNumber}`;
}