Skip to content

feat(studio): group rows in the timeline, and a split disclosure - #3286

Open
vanceingalls wants to merge 1 commit into
wa-23a-group-modelfrom
wa-23b-group-row
Open

feat(studio): group rows in the timeline, and a split disclosure#3286
vanceingalls wants to merge 1 commit into
wa-23a-group-modelfrom
wa-23b-group-row

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

Summary

  • A group renders as its own row with member rows beneath it; disclosure splits into two independent controls: caret shows/hides a group's member rows (structural), shows/hides any row's automation-lane rows.
  • Plain tracks lose their caret (nothing to disclose structurally) and keep only . expandedClipIds keeps its existing keyframe-lane-state job; expandedGroupIds/expandedLaneOwnerIds are new, independent sets.
  • TimelineLogicalRow.level widens 1 | 2 to 1 | 2 | 3 (group / member-under-group / lane), lanes always owner.level + 1.
  • All new UI/behavior is gated behind isCanaryEnabled("audio-groups"); disabled, groups resolves empty and every new code path no-ops.

Architecture note for reviewers

The original step doc assumed a group row could use the group's ID string as its position key in the timeline's row geometry. In practice rowGeometry/keyboard-nav/virtualization are built exclusively from real numeric tracks, so a synthetic string-keyed row would break focus, virtualization, and keyboard nav for that row (getRowIndex would silently return -1).

Per an explicit decision, this widens the row/geometry pipeline so group rows become real first-class rows — implemented via a synthetic fractional numeric anchor key (firstMemberTrack - 0.5, the same fractional-key convention sub-composition expansion already uses) rather than widening the key type to string | number. This achieves the same goal (group rows are real geometry rows, not visual-only overlays) with a smaller, less invasive diff. Group identity (used for expansion state, callbacks) stays the string groupId, carried on a separate field — only the position key is numeric.

Files touched for this: useTimelineTrackDerivations.ts (row re-emission + canary gate), useTimelineTrackLayout.ts (drag-preview insertion respects fractional keys), timelineKeyboardNavigation.ts (row model, level widening, group/track row emission), useTimelineLogicalRows.ts/useTimelineLogicalFocus.ts (threading), TimelineLanes.tsx (render wiring), new TimelineGroupHeader.tsx/TimelineGroupRow.tsx/useTimelineLaneRowIndexes.ts.

Stack

Depends on #3278 (B1), #3277 (P2), #3276 (P1), #3275 (A2), #3274 (A1) — should merge after all of them.

Test plan

  • bun run build clean
  • packages/studio full suite: 4245/4245 passed
  • Targeted: timelineKeyboardNavigation.test.ts, TimelineLanes.test.tsx, useTimelineLogicalRows.test.tsx, Timeline.test.ts
  • bunx oxfmt / bunx oxlint clean
  • fallow audit --base origin/main --fail-on-issues: complexity passes (0 new above-threshold findings introduced by this diff; 2 remaining are pre-existing in untouched audioFx.ts)
  • Manual: the four disclosure states from the design doc, reproduced in hyperframes preview with ?hf_canary_audio_groups=on

🤖 Generated with Claude Code

A group renders as its own row with member rows beneath it, and disclosure
splits into two independent controls: caret shows/hides a group's member
rows (structural), `∿` shows/hides any row's automation-lane rows. Plain
tracks lose their caret (nothing to disclose structurally) and keep only
`∿`. `expandedClipIds` keeps its existing keyframe-lane-state job;
`expandedGroupIds`/`expandedLaneOwnerIds` are new, independent sets.

Groups get a real position in the row/geometry pipeline rather than a
visual-only overlay: `useTimelineTrackDerivations` re-emits a group's member
tracks contiguously under a synthetic fractional anchor key
(firstMember - 0.5, the same fractional-key convention sub-composition
expansion already uses), so `rowGeometry`/keyboard-nav/virtualization treat
a group row as a first-class row without widening their key type away from
number. `TimelineLogicalRow.level` widens `1 | 2` to `1 | 2 | 3` (group /
member-under-group / lane), lanes always `owner.level + 1`.

All of it — grouped row emission, the header, the new expansion state — is
gated behind `isCanaryEnabled("audio-groups")`; disabled, `groups` resolves
empty and every new code path no-ops. `TimelineElement.audioGroup` (+
`audioGroupLabel`, resolved once per document via `resolveAudioGroups` from
B1) is parsed unconditionally, mirroring how `hidden`/`fxChain` already
flow DOM → manifest → TimelineElement — inert without the canary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant