@hyperframes/studio 0.8.11 → 0.8.13

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 (161) hide show
  1. package/dist/assets/{hyperframes-player-C6J6fsAD.js → hyperframes-player-BP9rShvS.js} +1 -1
  2. package/dist/assets/{index-1vMlv05L.js → index-CBm9bYHQ.js} +1 -1
  3. package/dist/assets/{index-BX0piiWc.js → index-CDerRsMw.js} +1 -1
  4. package/dist/assets/index-CQMHN4nk.js +431 -0
  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 +7646 -6514
  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/domSelectionTimelineMirror.ts +12 -3
  64. package/src/hooks/timelineAudioGroupCreate.ts +345 -0
  65. package/src/hooks/timelineAudioGroupVolume.test.ts +204 -0
  66. package/src/hooks/timelineAudioGroupVolume.ts +135 -3
  67. package/src/hooks/timelineEditingHelpers.test.ts +86 -0
  68. package/src/hooks/timelineEditingHelpers.ts +19 -7
  69. package/src/hooks/timelineElementFxAttribute.ts +0 -3
  70. package/src/hooks/timelineTrackVisibility.test.ts +95 -13
  71. package/src/hooks/timelineTrackVisibility.ts +6 -182
  72. package/src/hooks/useAudioGroupCarveAssignment.test.tsx +139 -0
  73. package/src/hooks/useBlockedTimelineEditToast.ts +27 -0
  74. package/src/hooks/useDomEditAttributeCommits.ts +13 -0
  75. package/src/hooks/useDomEditSession.test.tsx +57 -2
  76. package/src/hooks/useDomEditSession.ts +23 -24
  77. package/src/hooks/useDomSelectionSelectionGuards.test.ts +115 -0
  78. package/src/hooks/useEffectiveTimelineDuration.ts +9 -7
  79. package/src/hooks/useLivePlayheadTime.ts +3 -1
  80. package/src/hooks/useRemoveBackground.ts +70 -0
  81. package/src/hooks/useTimelineEditing.ts +5 -19
  82. package/src/hooks/useTimelineEditingTypes.ts +7 -0
  83. package/src/player/components/AutomationEnvelopePaths.tsx +54 -0
  84. package/src/player/components/AutomationSelectionMenu.tsx +4 -0
  85. package/src/player/components/LayerDisclosureRow.tsx +59 -25
  86. package/src/player/components/PlayerControls.tsx +0 -38
  87. package/src/player/components/Timeline.test.ts +40 -1
  88. package/src/player/components/Timeline.tsx +5 -2
  89. package/src/player/components/TimelineAutomationLane.test.tsx +180 -0
  90. package/src/player/components/TimelineAutomationLane.tsx +129 -36
  91. package/src/player/components/TimelineAutomationLaneSlot.tsx +22 -3
  92. package/src/player/components/TimelineFxButton.test.tsx +167 -21
  93. package/src/player/components/TimelineFxButton.tsx +156 -31
  94. package/src/player/components/TimelineGroupHeader.test.tsx +0 -5
  95. package/src/player/components/TimelineGroupHeader.tsx +100 -102
  96. package/src/player/components/TimelineGroupLaneLabels.tsx +99 -0
  97. package/src/player/components/TimelineGroupRow.test.tsx +222 -0
  98. package/src/player/components/TimelineGroupRow.tsx +140 -71
  99. package/src/player/components/TimelineLanes.test.tsx +3 -3
  100. package/src/player/components/TimelineLanes.tsx +25 -23
  101. package/src/player/components/TimelineTrackHeader.test.tsx +298 -13
  102. package/src/player/components/TimelineTrackHeader.tsx +301 -381
  103. package/src/player/components/TimelineTrackPlainHeader.test.tsx +47 -69
  104. package/src/player/components/TimelineTrackPlainHeader.tsx +64 -54
  105. package/src/player/components/automationLaneData.test.ts +94 -0
  106. package/src/player/components/automationLaneData.ts +23 -0
  107. package/src/player/components/automationLaneDragMath.ts +28 -0
  108. package/src/player/components/automationLaneGeometry.ts +52 -13
  109. package/src/player/components/groupAutomationElement.test.ts +51 -0
  110. package/src/player/components/groupAutomationElement.ts +37 -0
  111. package/src/player/components/timelineCallbacks.ts +6 -2
  112. package/src/player/components/timelineKeyboardNavigation.test.ts +2 -2
  113. package/src/player/components/timelineKeyboardNavigation.ts +25 -8
  114. package/src/player/components/timelineLayout.ts +0 -1
  115. package/src/player/components/timelineViewModel.ts +30 -1
  116. package/src/player/components/trackHeaderLabelRows.tsx +328 -0
  117. package/src/player/components/useAutomationLaneGestures.ts +66 -18
  118. package/src/player/components/useAutomationSegmentDrag.ts +110 -0
  119. package/src/player/components/useTimelineClipDisclosure.ts +41 -0
  120. package/src/player/components/useTimelineLaneRowIndexes.ts +1 -1
  121. package/src/player/components/useTimelineLogicalFocus.ts +2 -2
  122. package/src/player/components/useTimelineLogicalRows.test.tsx +2 -2
  123. package/src/player/components/useTimelineLogicalRows.ts +3 -3
  124. package/src/player/components/useTimelineTrackDerivations.ts +47 -8
  125. package/src/player/components/useTimelineTrackLayout.test.ts +119 -0
  126. package/src/player/components/useTimelineTrackLayout.ts +12 -4
  127. package/src/player/hooks/previewMessageRouter.ts +4 -11
  128. package/src/player/hooks/timelineSyncHydration.ts +395 -0
  129. package/src/player/hooks/useExpandedTimelineElements.test.ts +83 -0
  130. package/src/player/hooks/useExpandedTimelineElements.ts +40 -1
  131. package/src/player/hooks/useTimelinePlayer.ts +2 -7
  132. package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -219
  133. package/src/player/lib/automationStoreSync.test.ts +26 -0
  134. package/src/player/lib/automationStoreSync.ts +28 -4
  135. package/src/player/lib/runtimeAudioId.test.ts +58 -0
  136. package/src/player/lib/timelineDOM.test.ts +115 -0
  137. package/src/player/lib/timelineDOM.ts +3 -32
  138. package/src/player/lib/timelineElementHelpers.ts +27 -0
  139. package/src/player/lib/timelineGroupInfo.ts +134 -0
  140. package/src/player/lib/timelineIframeHelpers.test.ts +21 -0
  141. package/src/player/lib/timelineIframeHelpers.ts +17 -0
  142. package/src/player/store/keyframeSlice.ts +79 -5
  143. package/src/player/store/playerStore.ts +37 -47
  144. package/src/player/store/playerStoreDevHandle.ts +22 -0
  145. package/src/player/store/playerStoreSelection.ts +41 -0
  146. package/src/player/store/timelineElement.ts +32 -0
  147. package/src/utils/hmrStableContext.ts +64 -0
  148. package/src/utils/timelineInspector.test.ts +35 -1
  149. package/src/utils/timelineInspector.ts +38 -0
  150. package/dist/assets/index-Ba9zbpT9.css +0 -1
  151. package/dist/assets/index-DoW5v5eS.js +0 -428
  152. package/src/hooks/useAudioSoloBridge.ts +0 -61
  153. package/src/hooks/useGroupLevel.ts +0 -36
  154. package/src/player/components/TimelineGroupBusStrip.test.tsx +0 -146
  155. package/src/player/components/TimelineGroupBusStrip.tsx +0 -103
  156. package/src/player/components/TimelineSoloButton.tsx +0 -32
  157. package/src/player/store/audioSoloSlice.test.ts +0 -113
  158. package/src/player/store/audioSoloSlice.ts +0 -43
  159. package/src/player/store/groupLevels.ts +0 -33
  160. /package/dist/{chunk-ZALQ3CW7.js.map → chunk-6BT6DTB4.js.map} +0 -0
  161. /package/dist/{domEditingLayers-XRVXRTNX.js.map → domEditingLayers-URA7BLWE.js.map} +0 -0
@@ -101,6 +101,37 @@ function mount(dataAttributes: Record<string, string>, alone = false, voices = 2
101
101
  return { host, onSetAttributeQuiet, onSetAttributeLive };
102
102
  }
103
103
 
104
+ function mountGroup(memberStart: number) {
105
+ const bus = document.createElement("hf-audio-group");
106
+ bus.id = "voiceover";
107
+ document.body.append(bus);
108
+ const member = document.createElement("audio");
109
+ member.id = "vo-1";
110
+ member.setAttribute("data-audio-group", "voiceover");
111
+ member.setAttribute("data-start", String(memberStart));
112
+ member.setAttribute("data-duration", "5");
113
+ document.body.append(member);
114
+
115
+ const host = document.createElement("div");
116
+ document.body.append(host);
117
+ const selection = {
118
+ dataAttributes: { "fx-chain": CHAIN },
119
+ id: "voiceover",
120
+ element: bus,
121
+ tagName: "hf-audio-group",
122
+ } as unknown as DomEditSelection;
123
+ act(() => {
124
+ createRoot(host).render(
125
+ <AudioFxGroup
126
+ element={selection}
127
+ onSetAttributeQuiet={vi.fn()}
128
+ onSetAttributeLive={vi.fn()}
129
+ />,
130
+ );
131
+ });
132
+ return host;
133
+ }
134
+
104
135
  const rowFor = (host: HTMLElement, label: string): HTMLElement | null => {
105
136
  for (const row of Array.from(host.querySelectorAll<HTMLElement>(".hf-fx-row"))) {
106
137
  if (row.querySelector(".hf-fx-label")?.textContent === label) return row;
@@ -565,6 +596,20 @@ describe("AudioFxGroup dynamic carve", () => {
565
596
  expect(store().playbackRequest?.returnTo).toBe(42);
566
597
  });
567
598
 
599
+ it("seeks a group audition to the next member span", () => {
600
+ act(() =>
601
+ usePlayerStore.setState({
602
+ isPlaying: false,
603
+ currentTime: 2,
604
+ requestedSeekTime: null,
605
+ }),
606
+ );
607
+ const host = mountGroup(10);
608
+ hoverPreset(host);
609
+ expect(store().requestedSeekTime).toBe(10);
610
+ leaveShelf(host);
611
+ });
612
+
568
613
  it("leaves a transport the author started alone", () => {
569
614
  // Stopping their playback because they passed over a preset would be the
570
615
  // panel taking a decision nobody offered it.
@@ -1341,6 +1386,62 @@ describe("AudioFxGroup carve by default", () => {
1341
1386
  expect(onSetAttributeQuiet.mock.calls.some((c) => c[0] === "data-fx-carve")).toBe(false);
1342
1387
  expect(host.querySelector(".hf-fx-carve-module")).toBeNull();
1343
1388
  });
1389
+
1390
+ /**
1391
+ * Mount a track of the test's own naming as the selection, with siblings.
1392
+ *
1393
+ * `mount` always calls the selected track `bed`, which is what let the near-end
1394
+ * hole go unnoticed: nothing ever selected a track whose NAME said voice.
1395
+ */
1396
+ const mountNamed = (id: string, siblings: string[]) => {
1397
+ const onSetAttributeQuiet = vi.fn();
1398
+ const selected = document.createElement("audio");
1399
+ selected.id = id;
1400
+ document.body.append(selected);
1401
+ for (const other of siblings) {
1402
+ const el = document.createElement("audio");
1403
+ el.id = other;
1404
+ document.body.append(el);
1405
+ }
1406
+ const host = document.createElement("div");
1407
+ document.body.append(host);
1408
+ act(() => {
1409
+ createRoot(host).render(
1410
+ <AudioFxGroup
1411
+ element={
1412
+ {
1413
+ dataAttributes: { "fx-chain": "" },
1414
+ id,
1415
+ element: selected,
1416
+ } as unknown as DomEditSelection
1417
+ }
1418
+ onSetAttributeQuiet={onSetAttributeQuiet}
1419
+ onSetAttributeLive={vi.fn()}
1420
+ />,
1421
+ );
1422
+ });
1423
+ return { host, onSetAttributeQuiet };
1424
+ };
1425
+
1426
+ // The reported bug. A carve makes room in a bed for a voice, so a voice track
1427
+ // is the one thing that can never be the bed — `couldBeCarveSource` has said
1428
+ // as much since it was written, and nothing called it. Selecting a narration
1429
+ // clip offered it the module, found one candidate, and applied a carve nobody
1430
+ // asked for.
1431
+ it("never offers the carve on a track whose name says voice", () => {
1432
+ const { host, onSetAttributeQuiet } = mountNamed("vo-2", ["music-bed"]);
1433
+ expect(onSetAttributeQuiet.mock.calls.some((c) => c[0] === "data-fx-carve")).toBe(false);
1434
+ expect(host.querySelector(".hf-fx-carve-module")).toBeNull();
1435
+ });
1436
+
1437
+ // Offering is a suggestion, applying is a decision. An unnamed track keeps the
1438
+ // module — the author may know better than the name does — but nothing is
1439
+ // written until they say so.
1440
+ it("offers but does not apply on a track whose name says nothing", () => {
1441
+ const { host, onSetAttributeQuiet } = mountNamed("a1", ["vo-1"]);
1442
+ expect(host.querySelector(".hf-fx-carve-module")).not.toBeNull();
1443
+ expect(onSetAttributeQuiet.mock.calls.some((c) => c[0] === "data-fx-carve")).toBe(false);
1444
+ });
1344
1445
  });
1345
1446
 
1346
1447
  describe("AudioFxGroup carve source list", () => {
@@ -7,7 +7,7 @@
7
7
  * budget, and self-contained enough to test on its own.
8
8
  */
9
9
 
10
- import { useState } from "react";
10
+ import { useMemo, useState } from "react";
11
11
  import {
12
12
  HF_AUDIO_FX_ATTR,
13
13
  HF_AUDIO_FX_DATA_KEY,
@@ -40,12 +40,47 @@ import {
40
40
  } from "./propertyPanelAutomation";
41
41
  import type { DomEditSelection } from "./domEditingTypes";
42
42
  import { useLivePlayheadTime } from "../../hooks/useLivePlayheadTime";
43
+ import { usePlayerStore } from "../../player/store/playerStore";
44
+ import { isRevealedAudioFxRequestCurrent } from "../../player/store/keyframeSlice";
43
45
  import { FxSection } from "./propertyPanelFxSection.js";
44
46
  import { clipStart } from "./propertyPanelAudioFxGroupUtils.js";
45
47
  import { useFxChainObserved } from "./useFxChainObserved.js";
46
48
  import { useFxCarve } from "./useFxCarve.js";
49
+ import { audioFxSignalPath } from "./audioFxSignalPath.js";
50
+ import type { AuditionSpan } from "./useAuditionTransport.js";
51
+ import {
52
+ HF_AUDIO_GROUP_ATTR,
53
+ HF_AUDIO_GROUP_TAG,
54
+ resolveAudioGroups,
55
+ } from "@hyperframes/core/audio-groups";
47
56
  import { useFxLevelling } from "./useFxLevelling.js";
48
57
 
58
+ function auditionSpan(startRaw: string | undefined, durationRaw: string | undefined) {
59
+ const start = Number.parseFloat(startRaw ?? "");
60
+ const duration = Number.parseFloat(durationRaw ?? "");
61
+ return Number.isFinite(start) && Number.isFinite(duration) && duration > 0
62
+ ? { start, duration }
63
+ : null;
64
+ }
65
+
66
+ /** The selected clip, or every current member when the selected rack is a bus. */
67
+ function auditionSpansFor(element: DomEditSelection): AuditionSpan[] {
68
+ const own = auditionSpan(element.dataAttributes?.["start"], element.dataAttributes?.["duration"]);
69
+ if (own) return [own];
70
+ if (element.tagName?.toLowerCase() !== HF_AUDIO_GROUP_TAG || !element.id) return [];
71
+ const doc = element.element?.ownerDocument;
72
+ if (!doc) return [];
73
+ return [...doc.querySelectorAll(`audio[${HF_AUDIO_GROUP_ATTR}]`)]
74
+ .filter((member) => member.getAttribute(HF_AUDIO_GROUP_ATTR) === element.id)
75
+ .flatMap((member) => {
76
+ const span = auditionSpan(
77
+ member.getAttribute("data-start") ?? undefined,
78
+ member.getAttribute("data-duration") ?? undefined,
79
+ );
80
+ return span ? [span] : [];
81
+ });
82
+ }
83
+
49
84
  /**
50
85
  * Bridges the FX panel to the element/attribute world. Chain and carve are
51
86
  * serialised onto the element the way colour grading carries its config, so
@@ -112,6 +147,9 @@ export function AudioFxGroup({
112
147
  * came under the playhead.
113
148
  */
114
149
  const playhead = useLivePlayheadTime();
150
+ const revealRequest = usePlayerStore((s) => s.revealedAudioFxTarget);
151
+ const timelineProjectId = usePlayerStore((s) => s.timelineProjectId);
152
+ const timelineSessionEpoch = usePlayerStore((s) => s.timelineSessionEpoch);
115
153
  const localTime = playhead - clipStart(element.dataAttributes?.["start"]);
116
154
  const liveAutomationValues = ((): Map<string, number> => {
117
155
  const values = new Map<string, number>();
@@ -226,6 +264,31 @@ export function AudioFxGroup({
226
264
 
227
265
  const [analysing, setAnalysing] = useState(false);
228
266
 
267
+ // The rack's In/Out lines. Resolved from the live document because a group's
268
+ // membership lives on the members, so neither end of the routing can be read
269
+ // off the selected element alone.
270
+ // Keyed on the store's element array as well as the selection: membership is
271
+ // held by the MEMBERS, so a clip joining or leaving this group changes neither
272
+ // `element` nor its attributes, and the path went stale — "OUT to mix" on a
273
+ // clip that had just been grouped. `syncStoredGroupAttribute` and
274
+ // `updateElement` both replace the array, so its identity is the cheap signal
275
+ // that membership may have moved.
276
+ const storeElements = usePlayerStore((s) => s.elements);
277
+ const signalPath = useMemo(() => {
278
+ const doc = element.element?.ownerDocument;
279
+ return audioFxSignalPath(
280
+ element.tagName?.toLowerCase(),
281
+ element.id ?? undefined,
282
+ doc ? resolveAudioGroups(doc) : [],
283
+ );
284
+ // eslint-disable-next-line react-hooks/exhaustive-deps
285
+ }, [element, storeElements]);
286
+
287
+ // A bus has no span of its own, so resolve the live members. The
288
+ // `storeElements` subscription above rerenders this panel when membership
289
+ // changes without changing the selection.
290
+ const auditionSpans = auditionSpansFor(element);
291
+
229
292
  const { carvedAgainstBy, sourceOptions, setCarve } = useFxCarve(
230
293
  element,
231
294
  chain,
@@ -249,6 +312,32 @@ export function AudioFxGroup({
249
312
 
250
313
  return (
251
314
  <FxSection
315
+ // A lane's reveal request, but only when it names THIS element: the rack
316
+ // shows one element, and a request aimed at another must not reopen
317
+ // whatever happens to be mounted. Stale requests (other project, pre-
318
+ // reload session) are refused by the same check.
319
+ revealTarget={
320
+ revealRequest &&
321
+ revealRequest.elementKey === element.id &&
322
+ isRevealedAudioFxRequestCurrent(revealRequest, {
323
+ timelineProjectId,
324
+ timelineSessionEpoch,
325
+ })
326
+ ? revealRequest.automationTarget
327
+ : null
328
+ }
329
+ // Forwarded, not dropped: the section consumes by nonce, so without it a
330
+ // request never fires and a second click on the same lane is inert.
331
+ revealNonce={
332
+ revealRequest &&
333
+ revealRequest.elementKey === element.id &&
334
+ isRevealedAudioFxRequestCurrent(revealRequest, {
335
+ timelineProjectId,
336
+ timelineSessionEpoch,
337
+ })
338
+ ? revealRequest.nonce
339
+ : null
340
+ }
252
341
  // Locked while the carve is measuring. `analyse` captures the chain and
253
342
  // the automation BEFORE its fetch and decode, then rewrites the whole
254
343
  // attribute from that snapshot — so an effect added, or a knob committed,
@@ -273,7 +362,8 @@ export function AudioFxGroup({
273
362
  next.nodes.length ? serializeAudioFxChain(next) : null,
274
363
  )
275
364
  }
276
- onAuditionTransport={auditionTransport}
365
+ signalPath={signalPath}
366
+ onAuditionTransport={(on) => auditionTransport(on, auditionSpans)}
277
367
  onChainPreview={(next) =>
278
368
  // Live writes skip the preview refresh entirely, so dragging a knob no
279
369
  // longer reloads the composition and restarts playback on every pixel.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A collapsed group's header row in the flat inspector.
3
+ *
4
+ * Its own module so `PropertyPanelFlat.tsx` stays under the studio's 600-line
5
+ * cap; it reads only its arguments, which is what makes it separable.
6
+ */
7
+
8
+ import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
9
+ import { slugifyDesignInput } from "../../utils/designInputTracking";
10
+ import { FlatGroupHeader } from "./propertyPanelFlatPrimitives";
11
+ import type { FlatGroupDescriptor } from "./propertyPanelFlatDescriptors";
12
+
13
+ /** Its title, its one-line summary, and the entrance animation only the group
14
+ * just toggled gets. */
15
+ export function closedGroupHeader(
16
+ group: FlatGroupDescriptor,
17
+ toggleOpen: (id: string) => void,
18
+ justToggledIds: readonly string[],
19
+ ) {
20
+ return (
21
+ <DesignPanelInputProvider key={group.id} section={slugifyDesignInput(group.title)}>
22
+ <FlatGroupHeader
23
+ title={group.title}
24
+ isOpen={false}
25
+ onToggleOpen={() => toggleOpen(group.id)}
26
+ summary={group.summary}
27
+ animateEntrance={justToggledIds.includes(group.id)}
28
+ />
29
+ </DesignPanelInputProvider>
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Is the selection hidden RIGHT NOW.
35
+ *
36
+ * `selectedElementHidden` is derived from `timelineElements`, and an
37
+ * `<hf-audio-group>` is not one — it is a mixer bus, and the runtime no longer
38
+ * stamps timing on it, so it has no timeline row to carry a `hidden` flag. Its
39
+ * `data-hidden` lives only on the element, so the attribute is the fallback.
40
+ */
41
+ export function isSelectionHidden(
42
+ fromTimeline: boolean,
43
+ element: { dataAttributes?: Record<string, string | undefined> } | null | undefined,
44
+ ): boolean {
45
+ return fromTimeline || element?.dataAttributes?.["hidden"] != null;
46
+ }
@@ -172,3 +172,34 @@ export function FlatMotionSection({
172
172
  </div>
173
173
  );
174
174
  }
175
+
176
+ /**
177
+ * What the Motion section is called, and what its collapsed line says.
178
+ *
179
+ * "Motion" names the tween editor. On audio the section is Start/Duration/End
180
+ * and nothing else, so the label would promise what it no longer offers — and
181
+ * "Motion: 0 effects" on a sound is a category error, hence the span instead of
182
+ * a count.
183
+ *
184
+ * Keyed on the TAG by its caller, not on whether the effects half is showing:
185
+ * that half also disappears when a host simply has not wired the GSAP handlers,
186
+ * and a div in that state is still a thing that moves — renaming its section
187
+ * would be describing the host's wiring rather than the element.
188
+ */
189
+ export function motionSectionLabel(args: {
190
+ timingOnly: boolean;
191
+ start: number;
192
+ duration: number;
193
+ effectCount: number;
194
+ }): { title: string; summary: string } {
195
+ if (args.timingOnly) {
196
+ return {
197
+ title: "Timing",
198
+ summary: `${formatTimingValue(args.start)} – ${formatTimingValue(args.start + args.duration)}`,
199
+ };
200
+ }
201
+ return {
202
+ title: "Motion",
203
+ summary: `${args.effectCount} effect${args.effectCount === 1 ? "" : "s"}`,
204
+ };
205
+ }