@hyperframes/studio 0.8.10 → 0.8.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/assets/{hyperframes-player-DoTrWV8q.js → hyperframes-player-B243szNa.js} +1 -1
  2. package/dist/assets/{index-DV45YLqR.js → index-BTP86sD-.js} +1 -1
  3. package/dist/assets/index-BZfUJ2He.js +431 -0
  4. package/dist/assets/{index-BQs1fahB.js → index-DQ_lkuqY.js} +1 -1
  5. package/dist/assets/index-yGhfxxoL.css +1 -0
  6. package/dist/{chunk-ZALQ3CW7.js → chunk-6BT6DTB4.js} +2 -1
  7. package/dist/{domEditingLayers-XRVXRTNX.js → domEditingLayers-URA7BLWE.js} +2 -2
  8. package/dist/index.d.ts +84 -43
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +6440 -5514
  11. package/dist/index.js.map +1 -1
  12. package/package.json +8 -8
  13. package/src/App.tsx +0 -2
  14. package/src/components/StudioRightPanel.tsx +15 -57
  15. package/src/components/editor/PropertyPanel.test.tsx +101 -3
  16. package/src/components/editor/PropertyPanel.tsx +15 -1
  17. package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
  18. package/src/components/editor/PropertyPanelEmptyState.tsx +35 -20
  19. package/src/components/editor/PropertyPanelFlat.tsx +82 -21
  20. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +21 -0
  21. package/src/components/editor/TimelineFxPopover.tsx +24 -4
  22. package/src/components/editor/audioFxRevealTarget.test.ts +59 -0
  23. package/src/components/editor/audioFxRevealTarget.ts +113 -0
  24. package/src/components/editor/audioFxSignalPath.test.ts +50 -0
  25. package/src/components/editor/audioFxSignalPath.ts +71 -0
  26. package/src/components/editor/audioFxSummary.test.ts +8 -0
  27. package/src/components/editor/audioFxSummary.ts +28 -16
  28. package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +101 -0
  29. package/src/components/editor/propertyPanelAudioFxGroup.tsx +92 -2
  30. package/src/components/editor/propertyPanelFlatClosedGroup.tsx +46 -0
  31. package/src/components/editor/propertyPanelFlatMotionSection.tsx +31 -0
  32. package/src/components/editor/propertyPanelFxCarveModule.tsx +203 -125
  33. package/src/components/editor/propertyPanelFxControls.test.tsx +50 -0
  34. package/src/components/editor/propertyPanelFxControls.tsx +10 -2
  35. package/src/components/editor/propertyPanelFxEqModule.tsx +3 -0
  36. package/src/components/editor/propertyPanelFxNodeRow.tsx +11 -1
  37. package/src/components/editor/propertyPanelFxPresetMenu.tsx +12 -2
  38. package/src/components/editor/propertyPanelFxRackChain.tsx +12 -5
  39. package/src/components/editor/propertyPanelFxSection.test.tsx +42 -0
  40. package/src/components/editor/propertyPanelFxSection.tsx +63 -4
  41. package/src/components/editor/propertyPanelFxSectionTypes.ts +19 -0
  42. package/src/components/editor/useAudioFxRevealSection.ts +64 -0
  43. package/src/components/editor/useAuditionTransport.test.ts +36 -0
  44. package/src/components/editor/useAuditionTransport.ts +78 -0
  45. package/src/components/editor/useFxAudition.ts +12 -1
  46. package/src/components/editor/useFxCarve.ts +0 -0
  47. package/src/components/editor/useFxCarveGrouping.test.ts +118 -0
  48. package/src/components/editor/useFxCarveGrouping.ts +139 -4
  49. package/src/components/editor/useFxCarveNodes.ts +153 -0
  50. package/src/components/editor/useFxLevelling.ts +5 -34
  51. package/src/components/nle/NLEContext.tsx +3 -10
  52. package/src/components/nle/PreviewPane.tsx +0 -1
  53. package/src/components/nle/useTimelineEditCallbacks.ts +5 -1
  54. package/src/contexts/DesignPanelInputContext.tsx +6 -5
  55. package/src/contexts/DomEditContext.tsx +10 -3
  56. package/src/contexts/FileManagerContext.tsx +3 -2
  57. package/src/contexts/PanelLayoutContext.tsx +3 -2
  58. package/src/contexts/StudioContext.tsx +7 -3
  59. package/src/contexts/TimelineEditContext.tsx +6 -2
  60. package/src/contexts/VariablePromoteContext.tsx +6 -2
  61. package/src/contexts/ViewModeContext.tsx +2 -2
  62. package/src/hooks/domEditDeleteMembers.ts +34 -0
  63. package/src/hooks/timelineAudioGroupCreate.ts +345 -0
  64. package/src/hooks/timelineAudioGroupVolume.test.ts +204 -0
  65. package/src/hooks/timelineAudioGroupVolume.ts +135 -3
  66. package/src/hooks/timelineEditingHelpers.test.ts +86 -0
  67. package/src/hooks/timelineEditingHelpers.ts +19 -7
  68. package/src/hooks/timelineElementFxAttribute.ts +0 -3
  69. package/src/hooks/timelineTrackVisibility.test.ts +95 -13
  70. package/src/hooks/timelineTrackVisibility.ts +6 -182
  71. package/src/hooks/useAudioGroupCarveAssignment.test.tsx +139 -0
  72. package/src/hooks/useBlockedTimelineEditToast.ts +27 -0
  73. package/src/hooks/useDomEditAttributeCommits.ts +13 -0
  74. package/src/hooks/useDomEditSession.test.tsx +57 -2
  75. package/src/hooks/useDomEditSession.ts +23 -24
  76. package/src/hooks/useEffectiveTimelineDuration.ts +9 -7
  77. package/src/hooks/useLivePlayheadTime.ts +3 -1
  78. package/src/hooks/useRemoveBackground.ts +70 -0
  79. package/src/hooks/useTimelineEditing.ts +5 -19
  80. package/src/hooks/useTimelineEditingTypes.ts +7 -0
  81. package/src/player/components/AutomationSelectionMenu.tsx +4 -0
  82. package/src/player/components/LayerDisclosureRow.tsx +59 -25
  83. package/src/player/components/PlayerControls.tsx +0 -38
  84. package/src/player/components/Timeline.test.ts +40 -1
  85. package/src/player/components/Timeline.tsx +5 -2
  86. package/src/player/components/TimelineAutomationLane.test.tsx +67 -0
  87. package/src/player/components/TimelineAutomationLane.tsx +89 -12
  88. package/src/player/components/TimelineAutomationLaneSlot.tsx +22 -3
  89. package/src/player/components/TimelineFxButton.test.tsx +184 -7
  90. package/src/player/components/TimelineFxButton.tsx +171 -23
  91. package/src/player/components/TimelineGroupHeader.test.tsx +0 -5
  92. package/src/player/components/TimelineGroupHeader.tsx +100 -102
  93. package/src/player/components/TimelineGroupLaneLabels.tsx +88 -0
  94. package/src/player/components/TimelineGroupRow.test.tsx +146 -0
  95. package/src/player/components/TimelineGroupRow.tsx +123 -64
  96. package/src/player/components/TimelineLanes.test.tsx +3 -3
  97. package/src/player/components/TimelineLanes.tsx +25 -23
  98. package/src/player/components/TimelineTrackHeader.test.tsx +298 -13
  99. package/src/player/components/TimelineTrackHeader.tsx +301 -381
  100. package/src/player/components/TimelineTrackPlainHeader.test.tsx +47 -69
  101. package/src/player/components/TimelineTrackPlainHeader.tsx +64 -54
  102. package/src/player/components/automationLaneData.test.ts +94 -0
  103. package/src/player/components/automationLaneData.ts +23 -0
  104. package/src/player/components/groupAutomationElement.test.ts +51 -0
  105. package/src/player/components/groupAutomationElement.ts +37 -0
  106. package/src/player/components/timelineCallbacks.ts +6 -2
  107. package/src/player/components/timelineKeyboardNavigation.test.ts +2 -2
  108. package/src/player/components/timelineKeyboardNavigation.ts +25 -8
  109. package/src/player/components/timelineLayout.ts +0 -1
  110. package/src/player/components/timelineViewModel.ts +30 -1
  111. package/src/player/components/trackHeaderLabelRows.tsx +328 -0
  112. package/src/player/components/useTimelineClipDisclosure.ts +41 -0
  113. package/src/player/components/useTimelineLaneRowIndexes.ts +1 -1
  114. package/src/player/components/useTimelineLogicalFocus.ts +2 -2
  115. package/src/player/components/useTimelineLogicalRows.test.tsx +2 -2
  116. package/src/player/components/useTimelineLogicalRows.ts +3 -3
  117. package/src/player/components/useTimelineTrackDerivations.ts +47 -8
  118. package/src/player/components/useTimelineTrackLayout.test.ts +119 -0
  119. package/src/player/components/useTimelineTrackLayout.ts +12 -4
  120. package/src/player/hooks/previewMessageRouter.ts +4 -11
  121. package/src/player/hooks/timelineSyncHydration.ts +395 -0
  122. package/src/player/hooks/useExpandedTimelineElements.test.ts +83 -0
  123. package/src/player/hooks/useExpandedTimelineElements.ts +40 -1
  124. package/src/player/hooks/useTimelinePlayer.ts +2 -7
  125. package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -219
  126. package/src/player/lib/automationStoreSync.test.ts +26 -0
  127. package/src/player/lib/automationStoreSync.ts +28 -4
  128. package/src/player/lib/runtimeAudioId.test.ts +58 -0
  129. package/src/player/lib/timelineDOM.test.ts +115 -0
  130. package/src/player/lib/timelineDOM.ts +3 -32
  131. package/src/player/lib/timelineElementHelpers.ts +27 -0
  132. package/src/player/lib/timelineGroupInfo.ts +134 -0
  133. package/src/player/lib/timelineIframeHelpers.test.ts +21 -0
  134. package/src/player/lib/timelineIframeHelpers.ts +17 -0
  135. package/src/player/store/keyframeSlice.ts +79 -5
  136. package/src/player/store/playerStore.ts +37 -47
  137. package/src/player/store/playerStoreDevHandle.ts +22 -0
  138. package/src/player/store/playerStoreSelection.ts +41 -0
  139. package/src/player/store/timelineElement.ts +32 -0
  140. package/src/utils/hmrStableContext.ts +64 -0
  141. package/src/utils/timelineInspector.test.ts +35 -1
  142. package/src/utils/timelineInspector.ts +38 -0
  143. package/dist/assets/index-Ba9zbpT9.css +0 -1
  144. package/dist/assets/index-C6m2nHiX.js +0 -428
  145. package/src/hooks/useAudioSoloBridge.ts +0 -61
  146. package/src/hooks/useGroupLevel.ts +0 -36
  147. package/src/player/components/TimelineGroupBusStrip.test.tsx +0 -146
  148. package/src/player/components/TimelineGroupBusStrip.tsx +0 -103
  149. package/src/player/components/TimelineSoloButton.tsx +0 -32
  150. package/src/player/store/audioSoloSlice.test.ts +0 -113
  151. package/src/player/store/audioSoloSlice.ts +0 -43
  152. package/src/player/store/groupLevels.ts +0 -33
  153. /package/dist/{chunk-ZALQ3CW7.js.map → chunk-6BT6DTB4.js.map} +0 -0
  154. /package/dist/{domEditingLayers-XRVXRTNX.js.map → domEditingLayers-URA7BLWE.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import type { TimelineElement } from "../store/playerStore";
2
+ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
3
3
  import { getTrackStyle, type TrackVisualStyle } from "./timelineIcons";
4
4
 
5
5
  /** One resolved audio group, positioned in the row order. */
@@ -15,12 +15,26 @@ export interface TimelineTrackGroupInfo {
15
15
  anchorKey: number;
16
16
  /** Member track numbers, ascending. */
17
17
  memberTracks: number[];
18
+ /**
19
+ * Every clip under this group, in member-track order — INDEPENDENT of whether
20
+ * the group is expanded.
21
+ *
22
+ * Collapsing a group stops emitting its member rows into `tracks`, so anything
23
+ * that recovered member elements by looking them up there got an empty list in
24
+ * the default (collapsed) state — silently disabling the
25
+ * automation-lane count, and the bus strip's member labels. Membership is not
26
+ * a display concern, so it does not travel through the display list.
27
+ */
28
+ memberElements: TimelineElement[];
18
29
  /** The group element's `data-volume`, mirrored from a member's parse (B7's slider). */
19
30
  volume: number;
20
31
  /** The group element's `data-hidden`, mirrored from a member's parse (B5's group mute). */
21
32
  hidden: boolean;
22
33
  /** The group element's serialized `data-fx-chain`, mirrored from a member's parse (C1's FX entry). */
23
34
  fxChain?: string;
35
+ /** The group element's serialized `data-automation` — the group's OWN lanes,
36
+ * which are what its `∿` discloses (groups doc §5). */
37
+ automation?: string;
24
38
  }
25
39
 
26
40
  interface GroupMembership {
@@ -30,6 +44,7 @@ interface GroupMembership {
30
44
  volumeByGroup: Map<string, number>;
31
45
  hiddenByGroup: Map<string, boolean>;
32
46
  fxChainByGroup: Map<string, string | undefined>;
47
+ automationByGroup: Map<string, string | undefined>;
33
48
  }
34
49
 
35
50
  /** Which track belongs to which group, and each group's label/volume/hidden/fxChain — one pass over raw tracks. */
@@ -40,6 +55,7 @@ function resolveGroupMembership(rawTracks: [number, TimelineElement[]][]): Group
40
55
  const volumeByGroup = new Map<string, number>();
41
56
  const hiddenByGroup = new Map<string, boolean>();
42
57
  const fxChainByGroup = new Map<string, string | undefined>();
58
+ const automationByGroup = new Map<string, string | undefined>();
43
59
  for (const [trackNum, elements] of rawTracks) {
44
60
  const owner = elements.find((el) => el.audioGroup);
45
61
  if (!owner?.audioGroup) continue;
@@ -49,6 +65,7 @@ function resolveGroupMembership(rawTracks: [number, TimelineElement[]][]): Group
49
65
  volumeByGroup.set(owner.audioGroup, owner.audioGroupVolume ?? 1);
50
66
  hiddenByGroup.set(owner.audioGroup, owner.audioGroupHidden ?? false);
51
67
  fxChainByGroup.set(owner.audioGroup, owner.audioGroupFxChain);
68
+ automationByGroup.set(owner.audioGroup, owner.audioGroupAutomation);
52
69
  }
53
70
  const members = memberTracksByGroup.get(owner.audioGroup) ?? [];
54
71
  members.push(trackNum);
@@ -61,6 +78,7 @@ function resolveGroupMembership(rawTracks: [number, TimelineElement[]][]): Group
61
78
  volumeByGroup,
62
79
  hiddenByGroup,
63
80
  fxChainByGroup,
81
+ automationByGroup,
64
82
  };
65
83
  }
66
84
 
@@ -69,33 +87,50 @@ function buildGroupInfo(
69
87
  groupId: string,
70
88
  fallbackTrackNum: number,
71
89
  membership: GroupMembership,
90
+ rawByTrack: ReadonlyMap<number, TimelineElement[]>,
72
91
  ): TimelineTrackGroupInfo {
73
92
  const memberTracks = [...(membership.memberTracksByGroup.get(groupId) ?? [])].sort(
74
93
  (a, b) => a - b,
75
94
  );
76
95
  const fxChain = membership.fxChainByGroup.get(groupId);
96
+ const automation = membership.automationByGroup.get(groupId);
77
97
  return {
78
98
  id: groupId,
79
99
  label: membership.labelByGroup.get(groupId) ?? groupId,
100
+ // Exactly x.5, which sub-composition child rows are now kept strictly below
101
+ // (`useExpandedTimelineElements`) — they used to be able to land here and
102
+ // collide, duplicating the group header.
80
103
  anchorKey: (memberTracks[0] ?? fallbackTrackNum) - 0.5,
81
104
  memberTracks,
105
+ memberElements: memberTracks.flatMap((track) => rawByTrack.get(track) ?? []),
82
106
  volume: membership.volumeByGroup.get(groupId) ?? 1,
83
107
  hidden: membership.hiddenByGroup.get(groupId) ?? false,
84
108
  ...(fxChain ? { fxChain } : {}),
109
+ ...(automation ? { automation } : {}),
85
110
  };
86
111
  }
87
112
 
88
- /** Push a group's synthetic anchor row plus its members' rows, contiguously. */
113
+ /**
114
+ * Push a group's synthetic anchor row plus its members' rows, contiguously.
115
+ *
116
+ * A collapsed group emits its anchor and nothing else. `buildTimelineLogicalRows`
117
+ * already stops at the anchor when collapsed, so leaving the member rows in
118
+ * `tracks` left row geometry reserving a full row each for rows that then
119
+ * rendered as `null` — a header trailed by its members' worth of blank,
120
+ * unreachable dead space. Membership still lands in `trackGroupOf`: a collapsed
121
+ * member is hidden, not ungrouped.
122
+ */
89
123
  function emitGroupRows(
90
124
  info: TimelineTrackGroupInfo,
91
125
  rawByTrack: ReadonlyMap<number, TimelineElement[]>,
92
126
  trackGroupOf: Map<number, TimelineTrackGroupInfo>,
93
127
  tracks: [number, TimelineElement[]][],
128
+ expanded: boolean,
94
129
  ): void {
95
130
  tracks.push([info.anchorKey, []]);
96
131
  for (const member of info.memberTracks) {
97
132
  trackGroupOf.set(member, info);
98
- tracks.push([member, rawByTrack.get(member) ?? []]);
133
+ if (expanded) tracks.push([member, rawByTrack.get(member) ?? []]);
99
134
  }
100
135
  }
101
136
 
@@ -105,7 +140,10 @@ function emitGroupRows(
105
140
  * their position; a group's members move up to sit under its anchor even when
106
141
  * other (ungrouped) tracks were interleaved between them.
107
142
  */
108
- function groupTimelineTracks(rawTracks: [number, TimelineElement[]][]): {
143
+ function groupTimelineTracks(
144
+ rawTracks: [number, TimelineElement[]][],
145
+ collapsedGroupIds: ReadonlySet<string>,
146
+ ): {
109
147
  tracks: [number, TimelineElement[]][];
110
148
  groups: TimelineTrackGroupInfo[];
111
149
  trackGroupOf: Map<number, TimelineTrackGroupInfo>;
@@ -125,9 +163,9 @@ function groupTimelineTracks(rawTracks: [number, TimelineElement[]][]): {
125
163
  }
126
164
  if (emitted.has(groupId)) continue;
127
165
  emitted.add(groupId);
128
- const info = buildGroupInfo(groupId, trackNum, membership);
166
+ const info = buildGroupInfo(groupId, trackNum, membership, rawByTrack);
129
167
  groups.push(info);
130
- emitGroupRows(info, rawByTrack, trackGroupOf, tracks);
168
+ emitGroupRows(info, rawByTrack, trackGroupOf, tracks, !collapsedGroupIds.has(groupId));
131
169
  }
132
170
  return { tracks, groups, trackGroupOf };
133
171
  }
@@ -156,9 +194,10 @@ export function useTimelineTrackDerivations(expandedElements: TimelineElement[])
156
194
  return Array.from(map.entries()).sort(([a], [b]) => a - b);
157
195
  }, [expandedElements]);
158
196
 
197
+ const collapsedGroupIds = usePlayerStore((s) => s.collapsedGroupIds);
159
198
  const { tracks, groups, trackGroupOf } = useMemo(
160
- () => groupTimelineTracks(rawTracks),
161
- [rawTracks],
199
+ () => groupTimelineTracks(rawTracks, collapsedGroupIds),
200
+ [rawTracks, collapsedGroupIds],
162
201
  );
163
202
 
164
203
  const trackStyles = useMemo(() => {
@@ -38,6 +38,125 @@ function renderTrackLayout(
38
38
  return { layout, unmount: () => act(() => root.unmount()) };
39
39
  }
40
40
 
41
+ describe("collapsed audio groups", () => {
42
+ const member = (id: string, track: number): TimelineElement => ({
43
+ id,
44
+ domId: id,
45
+ tag: "audio",
46
+ start: 0,
47
+ duration: 5,
48
+ track,
49
+ audioGroup: "voiceover",
50
+ });
51
+
52
+ /** `collapsed` seeds the collapsed set — expanded is the default state. */
53
+ function renderGrouped(collapsed = false): {
54
+ layout: ReturnType<typeof useTimelineTrackLayout>;
55
+ unmount: () => void;
56
+ } {
57
+ if (collapsed) usePlayerStore.setState({ collapsedGroupIds: new Set(["voiceover"]) });
58
+ const elements = [member("voice-1", 0), member("voice-2", 1)];
59
+ let layout: ReturnType<typeof useTimelineTrackLayout> | undefined;
60
+ function Probe() {
61
+ layout = useTimelineTrackLayout(elements, new Map(), null, new Set());
62
+ return null;
63
+ }
64
+ const root = createRoot(document.createElement("div"));
65
+ act(() => root.render(React.createElement(Probe)));
66
+ if (!layout) throw new Error("Timeline track layout did not render");
67
+ return { layout, unmount: () => act(() => root.unmount()) };
68
+ }
69
+
70
+ // The `∿` area holds the group's own automation rows. Sized without them,
71
+ // every lane the count had just promised was clipped out of the row — which
72
+ // is what "expanding automation on a group doesn't show the automation"
73
+ // looked like from outside.
74
+ it("reserves room for the group's own automation rows, not just the strip", () => {
75
+ const automation = JSON.stringify({
76
+ version: 1,
77
+ lanes: [
78
+ {
79
+ target: "volume",
80
+ points: [
81
+ { t: 0, v: 1 },
82
+ { t: 5, v: 0.4 },
83
+ ],
84
+ },
85
+ ],
86
+ });
87
+ const elements = [
88
+ { ...member("voice-1", 0), audioGroupAutomation: automation },
89
+ { ...member("voice-2", 1), audioGroupAutomation: automation },
90
+ ];
91
+ let layout: ReturnType<typeof useTimelineTrackLayout> | undefined;
92
+ function Probe() {
93
+ layout = useTimelineTrackLayout(elements, new Map(), null, new Set());
94
+ return null;
95
+ }
96
+ const root = createRoot(document.createElement("div"));
97
+ act(() => {
98
+ usePlayerStore.setState({ expandedLaneOwnerIds: new Set(["voiceover"]) });
99
+ root.render(React.createElement(Probe));
100
+ });
101
+ // The group's anchor row: the first member track minus 0.5.
102
+ const anchorIndex = layout!.tracks.findIndex(([track]) => track === -0.5);
103
+ expect(anchorIndex).toBeGreaterThanOrEqual(0);
104
+ const openHeight = layout!.rowHeights[anchorIndex];
105
+ // One lane of headroom beyond the header row itself.
106
+ expect(openHeight).toBe(TRACK_H + AUTOMATION_LANE_H);
107
+ act(() => root.unmount());
108
+ });
109
+
110
+ // buildTimelineLogicalRows stops emitting member rows once a group is
111
+ // collapsed, so TimelineLanes renders null for them. Rows left in `tracks`
112
+ // still reserve height, turning that null into visible dead space — the row
113
+ // list and the logical rows have to agree.
114
+ it("emits only the anchor row while the group is collapsed", () => {
115
+ const { layout, unmount } = renderGrouped(true);
116
+ expect(layout.groups).toHaveLength(1);
117
+ expect(layout.groups[0]!.memberTracks).toEqual([0, 1]);
118
+ // The anchor (0 - 0.5) and nothing else.
119
+ expect(layout.trackOrder).toEqual([-0.5]);
120
+ expect(layout.rowGeometry.rowHeights).toHaveLength(1);
121
+ // Membership still resolves — collapsed is hidden, not ungrouped.
122
+ expect(layout.trackGroupOf.get(0)?.id).toBe("voiceover");
123
+ unmount();
124
+ });
125
+
126
+ // Membership is not a display concern. Half-lit solo, the automation-lane
127
+ // count and the bus strip's member labels all read the group's members, and
128
+ // all three silently degraded to empty when those were recovered from the
129
+ // display list — which a collapsed group does not appear in. Collapsed is the
130
+ // default, so that was every group until someone opened it.
131
+ it("carries its member elements even while collapsed", () => {
132
+ const { layout, unmount } = renderGrouped(true);
133
+ expect(layout.trackOrder).toEqual([-0.5]); // collapsed: no member rows
134
+ expect(layout.groups[0]!.memberElements.map((el) => el.id)).toEqual(["voice-1", "voice-2"]);
135
+ unmount();
136
+ });
137
+
138
+ // The reason the set is stored inverted. As an expanded-set, "absent" could
139
+ // not tell never-touched from deliberately-collapsed, so a freshly created
140
+ // group started collapsed — grouping three tracks made all three vanish
141
+ // behind a header the user had not yet learned to open.
142
+ it("is expanded by default, with nothing seeded", () => {
143
+ const { layout, unmount } = renderGrouped();
144
+ expect(usePlayerStore.getState().collapsedGroupIds.size).toBe(0);
145
+ expect(layout.trackOrder).toEqual([-0.5, 0, 1]);
146
+ unmount();
147
+ });
148
+
149
+ it("emits the member rows once the group is expanded", () => {
150
+ // Expanded is the default now — nothing to seed.
151
+ const { layout, unmount } = renderGrouped();
152
+ expect(layout.trackOrder).toEqual([-0.5, 0, 1]);
153
+ for (const track of layout.groups[0]!.memberTracks) {
154
+ expect(layout.rowGeometry.getRowHeight(layout.rowGeometry.getRowIndex(track))).toBe(TRACK_H);
155
+ }
156
+ unmount();
157
+ });
158
+ });
159
+
41
160
  describe("useTimelineTrackLayout", () => {
42
161
  it("counts a flat tween lane and reserves its expanded row height", () => {
43
162
  const elements: TimelineElement[] = [
@@ -7,7 +7,6 @@ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
7
7
  import type { DraggedClipState } from "./timelineClipDragTypes";
8
8
  import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
9
9
  import {
10
- STRIP_H,
11
10
  TRACK_H,
12
11
  createTimelineRowGeometry,
13
12
  type TimelineRowGeometry,
@@ -15,6 +14,13 @@ import {
15
14
  type TimelineTrackHeightClip,
16
15
  } from "./timelineLayout";
17
16
  import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
17
+ import { groupAutomationElement } from "./groupAutomationElement";
18
+ import { AUTOMATION_LANE_H } from "./automationLaneHeight";
19
+
20
+ /** Automation rows the GROUP itself owns — its `data-automation`, not its members'. */
21
+ function groupOwnLaneCount(group: TimelineTrackGroupInfo): number {
22
+ return groupAutomationLanes([groupAutomationElement(group, 0)]).length;
23
+ }
18
24
 
19
25
  export { getTrackStyle } from "./timelineIcons";
20
26
 
@@ -132,8 +138,8 @@ function computeLaneCounts(
132
138
  /** Group anchor rows have no elements of their own (`groupTimelineTracks`
133
139
  * pushes them as `[anchorKey, []]`), so `trackHeights` — which only ever
134
140
  * looks at a row's clips — always gives them TRACK_H. Override those
135
- * specific rows post-hoc: TRACK_H while collapsed, +STRIP_H once the
136
- * group's own `∿` (bus strip) is open. */
141
+ * specific rows post-hoc: TRACK_H while collapsed, plus the group's own
142
+ * automation rows once its `∿` is open. */
137
143
  function applyGroupStripHeights(
138
144
  tracks: readonly (readonly [number, readonly TimelineElement[]])[],
139
145
  rowHeights: number[],
@@ -145,7 +151,9 @@ function applyGroupStripHeights(
145
151
  return tracks.map(([track], index) => {
146
152
  const group = groupByAnchor.get(track);
147
153
  if (!group || !expandedLaneOwnerIds.has(group.id)) return rowHeights[index] ?? TRACK_H;
148
- return TRACK_H + STRIP_H;
154
+ // The group's own automation rows, which its `∿` discloses. A row sized
155
+ // without them clipped every lane it had just promised in the count.
156
+ return TRACK_H + groupOwnLaneCount(group) * AUTOMATION_LANE_H;
149
157
  });
150
158
  }
151
159
 
@@ -3,16 +3,16 @@
3
3
  *
4
4
  * Extracted from `useTimelinePlayer`, which had grown past the studio's 600-line
5
5
  * file cap and carried a `fallow-ignore-next-line complexity` on this function
6
- * admitting the same thing. Nothing here is new logic — it is the same four
7
- * branches (accept-gate, group levels, state, timeline) against the same refs,
8
- * with the suppression retired rather than moved.
6
+ * admitting the same thing. Nothing here is new logic — it is the same three
7
+ * branches (accept-gate, state, timeline) against the same refs, with the
8
+ * suppression retired rather than moved. The group-levels branch went with the
9
+ * level meter it fed (see the group volume/meter removal).
9
10
  */
10
11
 
11
12
  import { usePlayerStore, type TimelineElement } from "../store/playerStore";
12
13
  import type { ClipManifestClip, IframeWindow, PlaybackAdapter } from "../lib/playbackTypes";
13
14
  import { hasTimelinePerformanceFixtureLease } from "../lib/timelinePerformanceFixture";
14
15
  import { acceptStudioRuntimeMessage } from "../lib/runtimeProtocol";
15
- import { groupLevels, parseGroupLevelsMessage } from "../store/groupLevels";
16
16
  import { parseTimelineFromDOM } from "../lib/timelineDOM";
17
17
 
18
18
  /** What `processTimelineMessage` accepts — the clip-manifest postMessage. */
@@ -60,12 +60,6 @@ function acceptedPreviewMessage(
60
60
  return acceptStudioRuntimeMessage(data) ? data : null;
61
61
  }
62
62
 
63
- /** One meter reading per group with an active member. */
64
- function handleGroupLevelsMessage(data: PreviewMessage): void {
65
- const levels = parseGroupLevelsMessage(data);
66
- if (levels) groupLevels.notify(levels);
67
- }
68
-
69
63
  /**
70
64
  * A `state` tick doubles as a recovery hook: if the store still has no
71
65
  * elements, read the manifest straight off the iframe, and if no `timeline`
@@ -118,7 +112,6 @@ export function createPreviewMessageHandler(
118
112
  const iframe = deps.iframeRef.current;
119
113
  const data = acceptedPreviewMessage(e, iframe);
120
114
  if (!data) return;
121
- if (data.type === "group-levels") return handleGroupLevelsMessage(data);
122
115
  if (data.type === "state") return handleStateMessage(deps, iframe);
123
116
  if (data.type === "timeline" && Array.isArray(data.clips)) {
124
117
  handleTimelineMessage(deps, iframe, data as unknown as ClipManifestMessage);