@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.
- package/dist/assets/{hyperframes-player-DoTrWV8q.js → hyperframes-player-B243szNa.js} +1 -1
- package/dist/assets/{index-DV45YLqR.js → index-BTP86sD-.js} +1 -1
- package/dist/assets/index-BZfUJ2He.js +431 -0
- package/dist/assets/{index-BQs1fahB.js → index-DQ_lkuqY.js} +1 -1
- package/dist/assets/index-yGhfxxoL.css +1 -0
- package/dist/{chunk-ZALQ3CW7.js → chunk-6BT6DTB4.js} +2 -1
- package/dist/{domEditingLayers-XRVXRTNX.js → domEditingLayers-URA7BLWE.js} +2 -2
- package/dist/index.d.ts +84 -43
- package/dist/index.html +2 -2
- package/dist/index.js +6440 -5514
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/App.tsx +0 -2
- package/src/components/StudioRightPanel.tsx +15 -57
- package/src/components/editor/PropertyPanel.test.tsx +101 -3
- package/src/components/editor/PropertyPanel.tsx +15 -1
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +35 -20
- package/src/components/editor/PropertyPanelFlat.tsx +82 -21
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +21 -0
- package/src/components/editor/TimelineFxPopover.tsx +24 -4
- package/src/components/editor/audioFxRevealTarget.test.ts +59 -0
- package/src/components/editor/audioFxRevealTarget.ts +113 -0
- package/src/components/editor/audioFxSignalPath.test.ts +50 -0
- package/src/components/editor/audioFxSignalPath.ts +71 -0
- package/src/components/editor/audioFxSummary.test.ts +8 -0
- package/src/components/editor/audioFxSummary.ts +28 -16
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +101 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +92 -2
- package/src/components/editor/propertyPanelFlatClosedGroup.tsx +46 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +31 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +203 -125
- package/src/components/editor/propertyPanelFxControls.test.tsx +50 -0
- package/src/components/editor/propertyPanelFxControls.tsx +10 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +11 -1
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +12 -2
- package/src/components/editor/propertyPanelFxRackChain.tsx +12 -5
- package/src/components/editor/propertyPanelFxSection.test.tsx +42 -0
- package/src/components/editor/propertyPanelFxSection.tsx +63 -4
- package/src/components/editor/propertyPanelFxSectionTypes.ts +19 -0
- package/src/components/editor/useAudioFxRevealSection.ts +64 -0
- package/src/components/editor/useAuditionTransport.test.ts +36 -0
- package/src/components/editor/useAuditionTransport.ts +78 -0
- package/src/components/editor/useFxAudition.ts +12 -1
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxCarveGrouping.test.ts +118 -0
- package/src/components/editor/useFxCarveGrouping.ts +139 -4
- package/src/components/editor/useFxCarveNodes.ts +153 -0
- package/src/components/editor/useFxLevelling.ts +5 -34
- package/src/components/nle/NLEContext.tsx +3 -10
- package/src/components/nle/PreviewPane.tsx +0 -1
- package/src/components/nle/useTimelineEditCallbacks.ts +5 -1
- package/src/contexts/DesignPanelInputContext.tsx +6 -5
- package/src/contexts/DomEditContext.tsx +10 -3
- package/src/contexts/FileManagerContext.tsx +3 -2
- package/src/contexts/PanelLayoutContext.tsx +3 -2
- package/src/contexts/StudioContext.tsx +7 -3
- package/src/contexts/TimelineEditContext.tsx +6 -2
- package/src/contexts/VariablePromoteContext.tsx +6 -2
- package/src/contexts/ViewModeContext.tsx +2 -2
- package/src/hooks/domEditDeleteMembers.ts +34 -0
- package/src/hooks/timelineAudioGroupCreate.ts +345 -0
- package/src/hooks/timelineAudioGroupVolume.test.ts +204 -0
- package/src/hooks/timelineAudioGroupVolume.ts +135 -3
- package/src/hooks/timelineEditingHelpers.test.ts +86 -0
- package/src/hooks/timelineEditingHelpers.ts +19 -7
- package/src/hooks/timelineElementFxAttribute.ts +0 -3
- package/src/hooks/timelineTrackVisibility.test.ts +95 -13
- package/src/hooks/timelineTrackVisibility.ts +6 -182
- package/src/hooks/useAudioGroupCarveAssignment.test.tsx +139 -0
- package/src/hooks/useBlockedTimelineEditToast.ts +27 -0
- package/src/hooks/useDomEditAttributeCommits.ts +13 -0
- package/src/hooks/useDomEditSession.test.tsx +57 -2
- package/src/hooks/useDomEditSession.ts +23 -24
- package/src/hooks/useEffectiveTimelineDuration.ts +9 -7
- package/src/hooks/useLivePlayheadTime.ts +3 -1
- package/src/hooks/useRemoveBackground.ts +70 -0
- package/src/hooks/useTimelineEditing.ts +5 -19
- package/src/hooks/useTimelineEditingTypes.ts +7 -0
- package/src/player/components/AutomationSelectionMenu.tsx +4 -0
- package/src/player/components/LayerDisclosureRow.tsx +59 -25
- package/src/player/components/PlayerControls.tsx +0 -38
- package/src/player/components/Timeline.test.ts +40 -1
- package/src/player/components/Timeline.tsx +5 -2
- package/src/player/components/TimelineAutomationLane.test.tsx +67 -0
- package/src/player/components/TimelineAutomationLane.tsx +89 -12
- package/src/player/components/TimelineAutomationLaneSlot.tsx +22 -3
- package/src/player/components/TimelineFxButton.test.tsx +184 -7
- package/src/player/components/TimelineFxButton.tsx +171 -23
- package/src/player/components/TimelineGroupHeader.test.tsx +0 -5
- package/src/player/components/TimelineGroupHeader.tsx +100 -102
- package/src/player/components/TimelineGroupLaneLabels.tsx +88 -0
- package/src/player/components/TimelineGroupRow.test.tsx +146 -0
- package/src/player/components/TimelineGroupRow.tsx +123 -64
- package/src/player/components/TimelineLanes.test.tsx +3 -3
- package/src/player/components/TimelineLanes.tsx +25 -23
- package/src/player/components/TimelineTrackHeader.test.tsx +298 -13
- package/src/player/components/TimelineTrackHeader.tsx +301 -381
- package/src/player/components/TimelineTrackPlainHeader.test.tsx +47 -69
- package/src/player/components/TimelineTrackPlainHeader.tsx +64 -54
- package/src/player/components/automationLaneData.test.ts +94 -0
- package/src/player/components/automationLaneData.ts +23 -0
- package/src/player/components/groupAutomationElement.test.ts +51 -0
- package/src/player/components/groupAutomationElement.ts +37 -0
- package/src/player/components/timelineCallbacks.ts +6 -2
- package/src/player/components/timelineKeyboardNavigation.test.ts +2 -2
- package/src/player/components/timelineKeyboardNavigation.ts +25 -8
- package/src/player/components/timelineLayout.ts +0 -1
- package/src/player/components/timelineViewModel.ts +30 -1
- package/src/player/components/trackHeaderLabelRows.tsx +328 -0
- package/src/player/components/useTimelineClipDisclosure.ts +41 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +1 -1
- package/src/player/components/useTimelineLogicalFocus.ts +2 -2
- package/src/player/components/useTimelineLogicalRows.test.tsx +2 -2
- package/src/player/components/useTimelineLogicalRows.ts +3 -3
- package/src/player/components/useTimelineTrackDerivations.ts +47 -8
- package/src/player/components/useTimelineTrackLayout.test.ts +119 -0
- package/src/player/components/useTimelineTrackLayout.ts +12 -4
- package/src/player/hooks/previewMessageRouter.ts +4 -11
- package/src/player/hooks/timelineSyncHydration.ts +395 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +40 -1
- package/src/player/hooks/useTimelinePlayer.ts +2 -7
- package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -219
- package/src/player/lib/automationStoreSync.test.ts +26 -0
- package/src/player/lib/automationStoreSync.ts +28 -4
- package/src/player/lib/runtimeAudioId.test.ts +58 -0
- package/src/player/lib/timelineDOM.test.ts +115 -0
- package/src/player/lib/timelineDOM.ts +3 -32
- package/src/player/lib/timelineElementHelpers.ts +27 -0
- package/src/player/lib/timelineGroupInfo.ts +134 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +21 -0
- package/src/player/lib/timelineIframeHelpers.ts +17 -0
- package/src/player/store/keyframeSlice.ts +79 -5
- package/src/player/store/playerStore.ts +37 -47
- package/src/player/store/playerStoreDevHandle.ts +22 -0
- package/src/player/store/playerStoreSelection.ts +41 -0
- package/src/player/store/timelineElement.ts +32 -0
- package/src/utils/hmrStableContext.ts +64 -0
- package/src/utils/timelineInspector.test.ts +35 -1
- package/src/utils/timelineInspector.ts +38 -0
- package/dist/assets/index-Ba9zbpT9.css +0 -1
- package/dist/assets/index-C6m2nHiX.js +0 -428
- package/src/hooks/useAudioSoloBridge.ts +0 -61
- package/src/hooks/useGroupLevel.ts +0 -36
- package/src/player/components/TimelineGroupBusStrip.test.tsx +0 -146
- package/src/player/components/TimelineGroupBusStrip.tsx +0 -103
- package/src/player/components/TimelineSoloButton.tsx +0 -32
- package/src/player/store/audioSoloSlice.test.ts +0 -113
- package/src/player/store/audioSoloSlice.ts +0 -43
- package/src/player/store/groupLevels.ts +0 -33
- /package/dist/{chunk-ZALQ3CW7.js.map → chunk-6BT6DTB4.js.map} +0 -0
- /package/dist/{domEditingLayers-XRVXRTNX.js.map → domEditingLayers-URA7BLWE.js.map} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { SpeakerHigh, SpeakerSlash } from "@phosphor-icons/react";
|
|
2
1
|
import type { HfAudioFxChain } from "@hyperframes/core/audio-fx";
|
|
3
2
|
import { TRACK_H } from "./timelineLayout";
|
|
4
3
|
import type { TimelineTheme } from "./timelineTheme";
|
|
5
4
|
import { TimelineFxButton } from "./TimelineFxButton";
|
|
5
|
+
import type { AuditionSpan } from "../../components/editor/useAuditionTransport.js";
|
|
6
6
|
|
|
7
7
|
interface TimelineGroupHeaderProps {
|
|
8
8
|
label: string;
|
|
@@ -14,20 +14,13 @@ interface TimelineGroupHeaderProps {
|
|
|
14
14
|
laneCount: number;
|
|
15
15
|
isLaneOpen: boolean;
|
|
16
16
|
onToggleLanes: () => void;
|
|
17
|
-
/** The group element's own `data-hidden` — mutes every member at once. */
|
|
18
|
-
hidden: boolean;
|
|
19
|
-
onToggleHidden: () => void;
|
|
20
|
-
/** This group id is itself in the soloed set (fully lit). */
|
|
21
|
-
isSoloed: boolean;
|
|
22
|
-
/** Not soloed itself, but at least one member is (half-lit). */
|
|
23
|
-
isHalfLitSolo: boolean;
|
|
24
17
|
/** `add: true` (⌘/Ctrl-click) toggles membership; a plain click is exclusive. */
|
|
25
|
-
onToggleSolo: (options?: { add?: boolean }) => void;
|
|
26
18
|
/** C1: the group's serialized `data-fx-chain`, when set. */
|
|
27
19
|
fxChain?: string;
|
|
28
20
|
onFxChainChange: (next: HfAudioFxChain) => void;
|
|
29
21
|
onFxChainPreview?: (next: HfAudioFxChain) => void;
|
|
30
|
-
|
|
22
|
+
/** Member clips, so hovering a preset auditions where the group sounds. */
|
|
23
|
+
auditionSpans?: readonly AuditionSpan[];
|
|
31
24
|
onOpenFxRack: () => void;
|
|
32
25
|
columnWidth: number;
|
|
33
26
|
theme: TimelineTheme;
|
|
@@ -35,8 +28,57 @@ interface TimelineGroupHeaderProps {
|
|
|
35
28
|
|
|
36
29
|
/**
|
|
37
30
|
* A group's own row header: caret (member disclosure) + `▤` + label + count +
|
|
38
|
-
*
|
|
31
|
+
* FX + `∿ n` (lane disclosure).
|
|
39
32
|
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The group's name, which IS the way into its rack — a group is an element
|
|
36
|
+
* carrying `data-fx-chain`, so selecting it is what puts the chain in the
|
|
37
|
+
* property panel. Its own component because the header it sits in already
|
|
38
|
+
* carries six controls and was over the complexity gate with this inline.
|
|
39
|
+
*/
|
|
40
|
+
function GroupNameButton({
|
|
41
|
+
label,
|
|
42
|
+
memberCount,
|
|
43
|
+
onOpenFxRack,
|
|
44
|
+
}: {
|
|
45
|
+
label: string;
|
|
46
|
+
memberCount: number;
|
|
47
|
+
onOpenFxRack: () => void;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
tabIndex={-1}
|
|
53
|
+
aria-label={`Open ${label} effects`}
|
|
54
|
+
title="Open effects"
|
|
55
|
+
// No `flex-1`: the row's control group owns the slack now (`ml-auto`), so
|
|
56
|
+
// claiming it here would push the controls off the right edge — and the
|
|
57
|
+
// count with them, since it rides inside this button.
|
|
58
|
+
className="flex h-6 min-w-0 items-center gap-1.5 rounded border-0 bg-transparent p-0 text-left text-[11px] text-white hover:text-[#3CE6AC] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
59
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
60
|
+
onClick={(event) => {
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
onOpenFxRack();
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<span aria-hidden="true" className="shrink-0 text-[12px] leading-none text-white/50">
|
|
66
|
+
▤
|
|
67
|
+
</span>
|
|
68
|
+
{/* Wraps rather than truncating — a name that needs a hover to be read
|
|
69
|
+
is no use in a column you scan. */}
|
|
70
|
+
<span className="min-w-0 break-words text-left font-medium leading-tight">{label}</span>
|
|
71
|
+
<span
|
|
72
|
+
className="shrink-0 rounded-full bg-white/10 px-1 text-[9px] leading-[14px] tabular-nums text-white/55"
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
title={`${memberCount} tracks`}
|
|
75
|
+
>
|
|
76
|
+
{memberCount}
|
|
77
|
+
</span>
|
|
78
|
+
</button>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
40
82
|
export function TimelineGroupHeader({
|
|
41
83
|
label,
|
|
42
84
|
memberCount,
|
|
@@ -45,15 +87,10 @@ export function TimelineGroupHeader({
|
|
|
45
87
|
laneCount,
|
|
46
88
|
isLaneOpen,
|
|
47
89
|
onToggleLanes,
|
|
48
|
-
hidden,
|
|
49
|
-
onToggleHidden,
|
|
50
|
-
isSoloed,
|
|
51
|
-
isHalfLitSolo,
|
|
52
|
-
onToggleSolo,
|
|
53
90
|
fxChain,
|
|
54
91
|
onFxChainChange,
|
|
55
92
|
onFxChainPreview,
|
|
56
|
-
|
|
93
|
+
auditionSpans,
|
|
57
94
|
onOpenFxRack,
|
|
58
95
|
columnWidth,
|
|
59
96
|
theme,
|
|
@@ -71,16 +108,19 @@ export function TimelineGroupHeader({
|
|
|
71
108
|
borderRight: `1px solid ${theme.gutterBorder}`,
|
|
72
109
|
}}
|
|
73
110
|
>
|
|
111
|
+
{/* One line, like a track header's: caret and name, then every control
|
|
112
|
+
anchored to the right edge. The name wraps and the controls are
|
|
113
|
+
`shrink-0`, so they hold the edge and the name gives way — no second
|
|
114
|
+
line needed to keep five controls off the label. */}
|
|
74
115
|
<button
|
|
75
116
|
type="button"
|
|
76
117
|
tabIndex={-1}
|
|
77
118
|
aria-expanded={isExpanded}
|
|
78
119
|
aria-label={`${isExpanded ? "Hide" : "Show"} ${label} tracks`}
|
|
79
120
|
title={`${isExpanded ? "Hide" : "Show"} tracks`}
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// size rule at all, so there is no measured target to match.
|
|
121
|
+
// 13px mono, matching the property panel's preset-run caret
|
|
122
|
+
// (`hf-fx-preset-run-caret`) — the same disclosure, so the same glyph
|
|
123
|
+
// at the same size rather than a smaller one unique to this row.
|
|
84
124
|
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 font-mono text-[13px] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
85
125
|
isExpanded ? "text-white" : "text-white/55 hover:text-white"
|
|
86
126
|
}`}
|
|
@@ -90,91 +130,49 @@ export function TimelineGroupHeader({
|
|
|
90
130
|
onToggleExpanded();
|
|
91
131
|
}}
|
|
92
132
|
>
|
|
93
|
-
{/* Swapped, not rotated:
|
|
94
|
-
|
|
133
|
+
{/* Swapped, not rotated: the panel's caret swaps too, and a rotated
|
|
134
|
+
▸ sits off-centre in its box because the glyph is not square. */}
|
|
95
135
|
<span aria-hidden="true">{isExpanded ? "▾" : "▸"}</span>
|
|
96
136
|
</button>
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
title={hidden ? "Unmute group" : `Mute group ${label}`}
|
|
115
|
-
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${
|
|
116
|
-
hidden ? "text-[#3CE6AC] hover:text-white" : "text-white/55 hover:text-white"
|
|
117
|
-
}`}
|
|
118
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
119
|
-
onClick={(event) => {
|
|
120
|
-
event.stopPropagation();
|
|
121
|
-
onToggleHidden();
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
{hidden ? (
|
|
125
|
-
<SpeakerSlash size={14} weight="bold" aria-hidden="true" />
|
|
126
|
-
) : (
|
|
127
|
-
<SpeakerHigh size={14} weight="bold" aria-hidden="true" />
|
|
128
|
-
)}
|
|
129
|
-
</button>
|
|
130
|
-
<button
|
|
131
|
-
type="button"
|
|
132
|
-
tabIndex={-1}
|
|
133
|
-
aria-pressed={isSoloed}
|
|
134
|
-
aria-label="Hear only this"
|
|
135
|
-
title="Hear only this"
|
|
136
|
-
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[13px] font-semibold transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${
|
|
137
|
-
isSoloed
|
|
138
|
-
? "text-[#F5C542] hover:text-white"
|
|
139
|
-
: isHalfLitSolo
|
|
140
|
-
? "text-[#F5C542]/50 hover:text-white"
|
|
141
|
-
: "text-white/35 hover:text-white/75"
|
|
142
|
-
}`}
|
|
143
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
144
|
-
onClick={(event) => {
|
|
145
|
-
event.stopPropagation();
|
|
146
|
-
onToggleSolo({ add: event.metaKey || event.ctrlKey });
|
|
147
|
-
}}
|
|
148
|
-
>
|
|
149
|
-
<span aria-hidden="true">⌗</span>
|
|
150
|
-
</button>
|
|
151
|
-
<TimelineFxButton
|
|
152
|
-
fxChainRaw={fxChain}
|
|
153
|
-
onChainChange={onFxChainChange}
|
|
154
|
-
onChainPreview={onFxChainPreview}
|
|
155
|
-
onAuditionTransport={onFxAuditionTransport}
|
|
156
|
-
onOpenRack={onOpenFxRack}
|
|
157
|
-
/>
|
|
158
|
-
<button
|
|
159
|
-
type="button"
|
|
160
|
-
tabIndex={-1}
|
|
161
|
-
aria-expanded={isLaneOpen}
|
|
162
|
-
aria-label={`${isLaneOpen ? "Hide" : "Show"} ${label} lanes`}
|
|
163
|
-
title={`${isLaneOpen ? "Hide" : "Show"} lanes`}
|
|
164
|
-
className={`flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
165
|
-
isLaneOpen ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
|
166
|
-
}`}
|
|
167
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
168
|
-
onClick={(event) => {
|
|
169
|
-
event.stopPropagation();
|
|
170
|
-
onToggleLanes();
|
|
171
|
-
}}
|
|
172
|
-
>
|
|
173
|
-
<span aria-hidden="true">∿</span>
|
|
137
|
+
<GroupNameButton label={label} memberCount={memberCount} onOpenFxRack={onOpenFxRack} />
|
|
138
|
+
{/* `ml-auto` absorbs the slack the truncating name leaves, so the controls
|
|
139
|
+
sit on the edge whatever the name's length. */}
|
|
140
|
+
<div className="ml-auto flex shrink-0 items-center gap-1.5">
|
|
141
|
+
<TimelineFxButton
|
|
142
|
+
fxChainRaw={fxChain}
|
|
143
|
+
onChainChange={onFxChainChange}
|
|
144
|
+
onChainPreview={onFxChainPreview}
|
|
145
|
+
auditionSpans={auditionSpans}
|
|
146
|
+
onOpenRack={onOpenFxRack}
|
|
147
|
+
/>
|
|
148
|
+
{/* No lanes, no control: an author who opens it meets an empty row and
|
|
149
|
+
learns nothing. A track header already gates its own `∿` this way
|
|
150
|
+
(`disclosable`); the group's was the one that still offered a
|
|
151
|
+
disclosure over nothing. Automation appears by being written — from
|
|
152
|
+
the rack or a keyframe — not by opening this, so nothing is
|
|
153
|
+
unreachable while it is hidden. */}
|
|
174
154
|
{laneCount > 0 && (
|
|
175
|
-
<
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
tabIndex={-1}
|
|
158
|
+
aria-expanded={isLaneOpen}
|
|
159
|
+
aria-label={`${isLaneOpen ? "Hide" : "Show"} ${label} lanes`}
|
|
160
|
+
title={`${isLaneOpen ? "Hide" : "Show"} lanes`}
|
|
161
|
+
// Anchored right, matching every other header's lane toggle.
|
|
162
|
+
className={`ml-auto flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
|
163
|
+
isLaneOpen ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
|
164
|
+
}`}
|
|
165
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
166
|
+
onClick={(event) => {
|
|
167
|
+
event.stopPropagation();
|
|
168
|
+
onToggleLanes();
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<span aria-hidden="true">∿</span>
|
|
172
|
+
<span className="text-[9px] tabular-nums text-white/55">{laneCount}</span>
|
|
173
|
+
</button>
|
|
176
174
|
)}
|
|
177
|
-
</
|
|
175
|
+
</div>
|
|
178
176
|
</div>
|
|
179
177
|
);
|
|
180
178
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The label column beside a group's own automation lanes.
|
|
3
|
+
*
|
|
4
|
+
* The designs draw a group lane as `▤ Volume 0.42` on an accent rail — and
|
|
5
|
+
* the rail is load-bearing, not decoration: "Scope is carried by colour, not by
|
|
6
|
+
* depth — a lane the group owns has an accent rail and names the group; a
|
|
7
|
+
* clip's lane is neutral." Two lanes both called Volume, doing entirely
|
|
8
|
+
* different things, otherwise sit eight pixels apart with nothing to tell them
|
|
9
|
+
* apart.
|
|
10
|
+
*
|
|
11
|
+
* The number is the value AT THE PLAYHEAD, not the stored seed: a readout that
|
|
12
|
+
* showed the seed would stand still while the curve is audibly working.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { sampleAutomationLane } from "@hyperframes/core/audio-automation";
|
|
16
|
+
import { groupAutomationLanes } from "./automationLaneData";
|
|
17
|
+
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
|
18
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
19
|
+
import { useLivePlayheadTime } from "../../hooks/useLivePlayheadTime";
|
|
20
|
+
|
|
21
|
+
export function TimelineGroupLaneLabels({
|
|
22
|
+
groupElement,
|
|
23
|
+
groupLabel,
|
|
24
|
+
top,
|
|
25
|
+
columnWidth,
|
|
26
|
+
gutterBackground,
|
|
27
|
+
accentColor,
|
|
28
|
+
}: {
|
|
29
|
+
/** The group wearing a clip's shape — see `groupAutomationElement`. */
|
|
30
|
+
groupElement: TimelineElement;
|
|
31
|
+
groupLabel: string;
|
|
32
|
+
/** y of the first lane, matching the canvas slot's own offset. */
|
|
33
|
+
top: number;
|
|
34
|
+
columnWidth: number;
|
|
35
|
+
gutterBackground: string;
|
|
36
|
+
accentColor: string;
|
|
37
|
+
}) {
|
|
38
|
+
// The LIVE playhead, not the row's `currentTime` prop — that one only moves
|
|
39
|
+
// on seek, so the readout sat frozen while the curve was audibly working,
|
|
40
|
+
// which is precisely the failure this number exists to prevent.
|
|
41
|
+
const currentTime = useLivePlayheadTime();
|
|
42
|
+
// The SAME source the curves and the reserved height use
|
|
43
|
+
// (`TimelineGroupRow`), not raw `elementAutomationLanes`. Raw lanes are
|
|
44
|
+
// neither deduped by property nor filtered for resolvability, and the old
|
|
45
|
+
// `if (!parts) return null` consumed an index without drawing — so one
|
|
46
|
+
// unresolvable target slid every later label one row off the curve it names.
|
|
47
|
+
const laneGroups = groupAutomationLanes([groupElement]);
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
{laneGroups.map((laneGroup, index) => {
|
|
51
|
+
const lane = laneGroup.entries[0]?.lane;
|
|
52
|
+
if (!lane) return null;
|
|
53
|
+
const parts = { name: laneGroup.name, param: laneGroup.param };
|
|
54
|
+
// A group's clock is composition time (§1.3), so the playhead needs no
|
|
55
|
+
// clip-local rebase here — unlike a clip's lane.
|
|
56
|
+
const value = sampleAutomationLane(lane, currentTime);
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
key={lane.target}
|
|
60
|
+
data-group-lane-label={lane.target}
|
|
61
|
+
className="absolute left-0 flex items-center gap-1.5 overflow-hidden px-1.5 text-[10px] text-white/65"
|
|
62
|
+
style={{
|
|
63
|
+
top: top + index * AUTOMATION_LANE_H,
|
|
64
|
+
width: columnWidth,
|
|
65
|
+
height: AUTOMATION_LANE_H,
|
|
66
|
+
background: gutterBackground,
|
|
67
|
+
borderLeft: `2px solid ${accentColor}`,
|
|
68
|
+
}}
|
|
69
|
+
title={`${groupLabel} · ${parts.param ? `${parts.name} · ${parts.param}` : parts.name}`}
|
|
70
|
+
>
|
|
71
|
+
<span aria-hidden="true" className="shrink-0 text-[11px] text-white/40">
|
|
72
|
+
▤
|
|
73
|
+
</span>
|
|
74
|
+
<span className="flex min-w-0 flex-1 flex-col justify-center leading-tight">
|
|
75
|
+
<span className="truncate font-mono text-[9px] text-white/70">{parts.name}</span>
|
|
76
|
+
{parts.param ? (
|
|
77
|
+
<span className="truncate font-mono text-[9px] text-white/40">{parts.param}</span>
|
|
78
|
+
) : null}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="shrink-0 font-mono text-[9px] tabular-nums text-white/55">
|
|
81
|
+
{value.toFixed(2)}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { TimelineGroupRow } from "./TimelineGroupRow";
|
|
6
|
+
import { TimelineEditProvider } from "../../contexts/TimelineEditContext";
|
|
7
|
+
import { defaultTimelineTheme } from "./timelineTheme";
|
|
8
|
+
import type { TimelineTrackGroupInfo } from "./useTimelineTrackDerivations";
|
|
9
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
10
|
+
|
|
11
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
|
+
|
|
13
|
+
vi.mock("../../telemetry/canary", () => ({ isCanaryEnabled: () => true }));
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
document.body.innerHTML = "";
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const member = (id: string, track: number): TimelineElement => ({
|
|
20
|
+
id,
|
|
21
|
+
domId: id,
|
|
22
|
+
tag: "audio",
|
|
23
|
+
start: 0,
|
|
24
|
+
duration: 5,
|
|
25
|
+
track,
|
|
26
|
+
audioGroup: "voiceover",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const GROUP: TimelineTrackGroupInfo = {
|
|
30
|
+
id: "voiceover",
|
|
31
|
+
label: "Voiceover",
|
|
32
|
+
anchorKey: -0.5,
|
|
33
|
+
memberTracks: [0, 1],
|
|
34
|
+
memberElements: [member("vo-1", 0), member("vo-2", 1)],
|
|
35
|
+
volume: 1,
|
|
36
|
+
hidden: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function renderRow(overrides: Partial<TimelineTrackGroupInfo> = {}) {
|
|
40
|
+
const onSetAudioGroupAttributeQuiet = vi.fn();
|
|
41
|
+
const onSetElementAttributeQuiet = vi.fn();
|
|
42
|
+
const host = document.createElement("div");
|
|
43
|
+
document.body.append(host);
|
|
44
|
+
act(() =>
|
|
45
|
+
createRoot(host).render(
|
|
46
|
+
<TimelineEditProvider value={{ onSetAudioGroupAttributeQuiet, onSetElementAttributeQuiet }}>
|
|
47
|
+
<TimelineGroupRow
|
|
48
|
+
index={0}
|
|
49
|
+
rowKey={0}
|
|
50
|
+
group={{ ...GROUP, ...overrides }}
|
|
51
|
+
logicalRow={{ id: "g", level: 1, kind: "track" } as never}
|
|
52
|
+
top={0}
|
|
53
|
+
height={48}
|
|
54
|
+
virtualized={false}
|
|
55
|
+
contentOrigin={232}
|
|
56
|
+
theme={defaultTimelineTheme}
|
|
57
|
+
collapsedGroupIds={new Set()}
|
|
58
|
+
expandedLaneOwnerIds={new Set()}
|
|
59
|
+
toggleGroupExpanded={vi.fn()}
|
|
60
|
+
toggleLaneOwnerExpanded={vi.fn()}
|
|
61
|
+
lanes={{ bind: () => ({ lanes: [] }) } as never}
|
|
62
|
+
pps={10}
|
|
63
|
+
currentTime={0}
|
|
64
|
+
compositionDuration={60}
|
|
65
|
+
contentGutter={0}
|
|
66
|
+
trackContentWidth={800}
|
|
67
|
+
/>
|
|
68
|
+
</TimelineEditProvider>,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
return { host, onSetAudioGroupAttributeQuiet, onSetElementAttributeQuiet };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("TimelineGroupRow", () => {
|
|
75
|
+
// C1 names this as the step's own definition of done: "opening the popover on
|
|
76
|
+
// a GROUP and applying a preset results in exactly ONE `data-fx-chain` write,
|
|
77
|
+
// on the group element, and zero writes on members". A group IS a bus — a
|
|
78
|
+
// write that fanned out to the members would be batch-apply wearing a bus's
|
|
79
|
+
// clothes, which is the one thing §1 rules out.
|
|
80
|
+
it("applies a preset to the group element only, never to its members", () => {
|
|
81
|
+
const { host, onSetAudioGroupAttributeQuiet, onSetElementAttributeQuiet } = renderRow();
|
|
82
|
+
const fx = Array.from(host.querySelectorAll("button")).find((b) =>
|
|
83
|
+
b.getAttribute("aria-label")?.startsWith("Effects"),
|
|
84
|
+
);
|
|
85
|
+
act(() => fx?.click());
|
|
86
|
+
const preset = document.querySelector<HTMLButtonElement>(".hf-fx-preset-item");
|
|
87
|
+
act(() => preset?.click());
|
|
88
|
+
|
|
89
|
+
expect(onSetAudioGroupAttributeQuiet).toHaveBeenCalledTimes(1);
|
|
90
|
+
const [groupId, attr] = onSetAudioGroupAttributeQuiet.mock.calls[0] ?? [];
|
|
91
|
+
expect(groupId).toBe("voiceover");
|
|
92
|
+
expect(attr).toBe("data-fx-chain");
|
|
93
|
+
// The members are the point: not one write reaches them.
|
|
94
|
+
expect(onSetElementAttributeQuiet).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// A disclosure over nothing tells the author their group has no automation
|
|
98
|
+
// only AFTER they open an empty row. Track headers already gate their own
|
|
99
|
+
// toggle on having something to disclose; the group's did not.
|
|
100
|
+
it("hides the lane toggle until the group actually automates something", () => {
|
|
101
|
+
const laneToggle = (host: HTMLElement) =>
|
|
102
|
+
Array.from(host.querySelectorAll("button")).find((b) =>
|
|
103
|
+
/lanes$/.test(b.getAttribute("aria-label") ?? ""),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(laneToggle(renderRow().host)).toBeUndefined();
|
|
107
|
+
|
|
108
|
+
const automated = renderRow({
|
|
109
|
+
fxChain: JSON.stringify({
|
|
110
|
+
version: 1,
|
|
111
|
+
nodes: [{ type: "peaking", id: "p1", params: { frequency: 1000, gain: -3, q: 1 } }],
|
|
112
|
+
}),
|
|
113
|
+
automation: JSON.stringify({
|
|
114
|
+
version: 1,
|
|
115
|
+
lanes: [{ target: "fx.p1.gain", points: [{ t: 0, v: 0 }] }],
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
expect(laneToggle(automated.host)).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Same shape as a track header: caret and name on the left, every control in
|
|
122
|
+
// one right-anchored group. It was two lines — name, then controls — which is
|
|
123
|
+
// what let a stray child overflow the 48px box on the track side.
|
|
124
|
+
it("keeps the caret, the name and every control on one line", () => {
|
|
125
|
+
const { host } = renderRow({
|
|
126
|
+
fxChain: JSON.stringify({
|
|
127
|
+
version: 1,
|
|
128
|
+
nodes: [{ type: "peaking", id: "p1", params: { frequency: 1000, gain: -3, q: 1 } }],
|
|
129
|
+
}),
|
|
130
|
+
automation: JSON.stringify({
|
|
131
|
+
version: 1,
|
|
132
|
+
lanes: [{ target: "fx.p1.gain", points: [{ t: 0, v: 0 }] }],
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
const header = host.querySelector<HTMLElement>('[role="rowheader"]');
|
|
136
|
+
// Caret, name, control group — no second line.
|
|
137
|
+
expect(header?.children).toHaveLength(3);
|
|
138
|
+
const controls = header?.lastElementChild as HTMLElement | null;
|
|
139
|
+
expect(controls?.className).toContain("ml-auto");
|
|
140
|
+
// Both controls live in that group, so they share the right edge.
|
|
141
|
+
expect(controls?.querySelectorAll("button")).toHaveLength(2);
|
|
142
|
+
// The member count rides with the name, not out with the controls.
|
|
143
|
+
expect(controls?.querySelector('[title="2 tracks"]')).toBeNull();
|
|
144
|
+
act(() => undefined);
|
|
145
|
+
});
|
|
146
|
+
});
|