@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.
- package/dist/assets/{hyperframes-player-C6J6fsAD.js → hyperframes-player-BP9rShvS.js} +1 -1
- package/dist/assets/{index-1vMlv05L.js → index-CBm9bYHQ.js} +1 -1
- package/dist/assets/{index-BX0piiWc.js → index-CDerRsMw.js} +1 -1
- package/dist/assets/index-CQMHN4nk.js +431 -0
- 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 +7646 -6514
- 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/domSelectionTimelineMirror.ts +12 -3
- 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/useDomSelectionSelectionGuards.test.ts +115 -0
- 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/AutomationEnvelopePaths.tsx +54 -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 +180 -0
- package/src/player/components/TimelineAutomationLane.tsx +129 -36
- package/src/player/components/TimelineAutomationLaneSlot.tsx +22 -3
- package/src/player/components/TimelineFxButton.test.tsx +167 -21
- package/src/player/components/TimelineFxButton.tsx +156 -31
- package/src/player/components/TimelineGroupHeader.test.tsx +0 -5
- package/src/player/components/TimelineGroupHeader.tsx +100 -102
- package/src/player/components/TimelineGroupLaneLabels.tsx +99 -0
- package/src/player/components/TimelineGroupRow.test.tsx +222 -0
- package/src/player/components/TimelineGroupRow.tsx +140 -71
- 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/automationLaneDragMath.ts +28 -0
- package/src/player/components/automationLaneGeometry.ts +52 -13
- 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/useAutomationLaneGestures.ts +66 -18
- package/src/player/components/useAutomationSegmentDrag.ts +110 -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-DoW5v5eS.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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.13",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"gsap": "^3.13.0",
|
|
48
48
|
"marked": "^14.1.4",
|
|
49
49
|
"mediabunny": "^1.45.3",
|
|
50
|
-
"@hyperframes/
|
|
51
|
-
"@hyperframes/
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/
|
|
54
|
-
"@hyperframes/
|
|
50
|
+
"@hyperframes/parsers": "0.8.13",
|
|
51
|
+
"@hyperframes/core": "0.8.13",
|
|
52
|
+
"@hyperframes/studio-server": "0.8.13",
|
|
53
|
+
"@hyperframes/sdk": "0.8.13",
|
|
54
|
+
"@hyperframes/player": "0.8.13"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "19",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"vite": "^6.4.2",
|
|
69
69
|
"vitest": "^3.2.4",
|
|
70
70
|
"zustand": "^5.0.0",
|
|
71
|
-
"@hyperframes/producer": "0.8.
|
|
71
|
+
"@hyperframes/producer": "0.8.13"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"react": "19",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"zustand": "^4.0.0 || ^5.0.0"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
|
-
"dev": "vite",
|
|
79
|
+
"dev": "bun --bun ./node_modules/.bin/vite --host 127.0.0.1",
|
|
80
80
|
"build": "vite build && tsup",
|
|
81
81
|
"typecheck": "tsc --noEmit",
|
|
82
82
|
"test": "vitest run",
|
package/src/App.tsx
CHANGED
|
@@ -38,7 +38,6 @@ import { useToast } from "./hooks/useToast";
|
|
|
38
38
|
import { useCompositionContentLoader } from "./hooks/useCompositionContentLoader";
|
|
39
39
|
import { useStudioUrlState } from "./hooks/useStudioUrlState";
|
|
40
40
|
import { useEffectiveTimelineDuration } from "./hooks/useEffectiveTimelineDuration";
|
|
41
|
-
import { useAudioSoloBridge } from "./hooks/useAudioSoloBridge";
|
|
42
41
|
import {
|
|
43
42
|
buildStudioContextValue,
|
|
44
43
|
useGlobalFileDrop,
|
|
@@ -82,7 +81,6 @@ export function StudioApp() {
|
|
|
82
81
|
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
|
|
83
82
|
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
|
|
84
83
|
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
85
|
-
useAudioSoloBridge(previewIframeRef);
|
|
86
84
|
const activeCompPathRef = useRef(activeCompPath);
|
|
87
85
|
activeCompPathRef.current = activeCompPath;
|
|
88
86
|
const leftSidebarRef = useRef<LeftSidebarHandle>(null);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback
|
|
1
|
+
import { useCallback } from "react";
|
|
2
2
|
import type { StudioRightPanelProps } from "./StudioRightPanel.types";
|
|
3
3
|
|
|
4
4
|
export type { StudioRightPanelProps };
|
|
@@ -20,15 +20,15 @@ import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
|
20
20
|
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
21
21
|
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
22
22
|
import { usePlayerStore } from "../player";
|
|
23
|
-
import { waitForMediaJob } from "./studioMediaJobs";
|
|
24
23
|
import {
|
|
25
24
|
applyColorGradingScopeUpdate,
|
|
26
25
|
EMPTY_COLOR_GRADING_SCOPE_RESULT,
|
|
27
26
|
type ColorGradingScope,
|
|
28
27
|
} from "./studioColorGradingScope";
|
|
29
|
-
import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
|
|
30
28
|
import { timelineKeysForSelections } from "../utils/studioHelpers";
|
|
29
|
+
import { canHideSelections } from "../utils/timelineInspector";
|
|
31
30
|
import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";
|
|
31
|
+
import { useRemoveBackground } from "../hooks/useRemoveBackground";
|
|
32
32
|
|
|
33
33
|
// fallow-ignore-next-line complexity
|
|
34
34
|
export function StudioRightPanel({
|
|
@@ -164,14 +164,6 @@ export function StudioRightPanel({
|
|
|
164
164
|
handleInspectorSplitResizeMove,
|
|
165
165
|
handleInspectorSplitResizeEnd,
|
|
166
166
|
} = useInspectorSplitResize();
|
|
167
|
-
const backgroundRemovalAbortRef = useRef<AbortController | null>(null);
|
|
168
|
-
|
|
169
|
-
useEffect(
|
|
170
|
-
() => () => {
|
|
171
|
-
backgroundRemovalAbortRef.current?.abort();
|
|
172
|
-
},
|
|
173
|
-
[],
|
|
174
|
-
);
|
|
175
167
|
|
|
176
168
|
const renderJobs = renderQueue.jobs as RenderJob[];
|
|
177
169
|
const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
|
|
@@ -238,52 +230,7 @@ export function StudioRightPanel({
|
|
|
238
230
|
],
|
|
239
231
|
);
|
|
240
232
|
|
|
241
|
-
const handleRemoveBackground =
|
|
242
|
-
// fallow-ignore-next-line complexity
|
|
243
|
-
async (
|
|
244
|
-
inputPath: string,
|
|
245
|
-
options: {
|
|
246
|
-
createBackgroundPlate?: boolean;
|
|
247
|
-
quality?: "fast" | "balanced" | "best";
|
|
248
|
-
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
|
249
|
-
},
|
|
250
|
-
) => {
|
|
251
|
-
const response = await fetch(
|
|
252
|
-
`/api/projects/${encodeURIComponent(projectId)}/media/remove-background`,
|
|
253
|
-
{
|
|
254
|
-
method: "POST",
|
|
255
|
-
headers: { "Content-Type": "application/json" },
|
|
256
|
-
body: JSON.stringify({
|
|
257
|
-
inputPath,
|
|
258
|
-
createBackgroundPlate: options.createBackgroundPlate === true,
|
|
259
|
-
quality: options.quality ?? "balanced",
|
|
260
|
-
}),
|
|
261
|
-
},
|
|
262
|
-
);
|
|
263
|
-
const data = (await response.json().catch(() => ({}))) as {
|
|
264
|
-
jobId?: string;
|
|
265
|
-
error?: string;
|
|
266
|
-
};
|
|
267
|
-
if (!response.ok || !data.jobId) {
|
|
268
|
-
throw new Error(data.error || `Background removal failed (${response.status})`);
|
|
269
|
-
}
|
|
270
|
-
showToast("Removing background...", "info");
|
|
271
|
-
backgroundRemovalAbortRef.current?.abort();
|
|
272
|
-
const controller = new AbortController();
|
|
273
|
-
backgroundRemovalAbortRef.current = controller;
|
|
274
|
-
try {
|
|
275
|
-
const result = await waitForMediaJob(data.jobId, options.onProgress, controller.signal);
|
|
276
|
-
await refreshFileTree();
|
|
277
|
-
showToast(`Created transparent asset: ${result.outputPath.split("/").pop()}`, "info");
|
|
278
|
-
return result;
|
|
279
|
-
} finally {
|
|
280
|
-
if (backgroundRemovalAbortRef.current === controller) {
|
|
281
|
-
backgroundRemovalAbortRef.current = null;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
[projectId, refreshFileTree, showToast],
|
|
286
|
-
);
|
|
233
|
+
const handleRemoveBackground = useRemoveBackground(projectId, refreshFileTree, showToast);
|
|
287
234
|
|
|
288
235
|
/**
|
|
289
236
|
* A dial being dragged writes to the preview and stops there.
|
|
@@ -300,6 +247,17 @@ export function StudioRightPanel({
|
|
|
300
247
|
[handleDomAttributeLiveCommit],
|
|
301
248
|
);
|
|
302
249
|
const handleHideAllSelected = () => {
|
|
250
|
+
// Audio has no visual to hide, and `data-hidden` on an audio element is what
|
|
251
|
+
// MUTES it — preview silences it and the render drops it from the mix. The
|
|
252
|
+
// timeline withholds the eye on an audio track for that reason
|
|
253
|
+
// (`visible={!isAudioTrack}`), and the single-selection panel gates the same
|
|
254
|
+
// write on `audioSelection`; this multi-selection path was the way back to
|
|
255
|
+
// it. Checked here as well as in the panel because the button is not the
|
|
256
|
+
// only caller.
|
|
257
|
+
if (!canHideSelections(domEditGroupSelections)) {
|
|
258
|
+
showToast("Audio can't be hidden — use the group's own controls", "info");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
303
261
|
const { elements } = usePlayerStore.getState();
|
|
304
262
|
const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath);
|
|
305
263
|
if (keys.length > 0) void onToggleElementHidden?.(keys, true);
|
|
@@ -22,7 +22,7 @@ afterEach(() => {
|
|
|
22
22
|
vi.resetModules();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
function baseElement() {
|
|
25
|
+
function baseElement(): NonNullable<PropertyPanelProps["element"]> {
|
|
26
26
|
return {
|
|
27
27
|
element: document.createElement("div"),
|
|
28
28
|
id: "mono-label",
|
|
@@ -81,7 +81,7 @@ function nonTextElement() {
|
|
|
81
81
|
// flat multi-field layer list (FlatTextLayerList + FlatTextFieldEditor) —
|
|
82
82
|
// must not double-render the "Text" heading (FlatGroup's own heading; this
|
|
83
83
|
// component never renders one of its own).
|
|
84
|
-
function multiFieldTextElement() {
|
|
84
|
+
function multiFieldTextElement(): NonNullable<PropertyPanelProps["element"]> {
|
|
85
85
|
const base = baseElement();
|
|
86
86
|
return {
|
|
87
87
|
...base,
|
|
@@ -177,6 +177,43 @@ function sixGroupElement() {
|
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** An `<audio>` clip: placed on the timeline, but nothing a tween could move. */
|
|
181
|
+
function audioClipElement() {
|
|
182
|
+
const element = document.createElement("audio");
|
|
183
|
+
return {
|
|
184
|
+
...baseElement(),
|
|
185
|
+
element,
|
|
186
|
+
id: "vo-1",
|
|
187
|
+
selector: "#vo-1",
|
|
188
|
+
label: "Vo 1",
|
|
189
|
+
tagName: "audio",
|
|
190
|
+
textFields: [],
|
|
191
|
+
dataAttributes: { start: "1", duration: "3" },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A mixer bus: no clip range at all, and no box either.
|
|
197
|
+
*
|
|
198
|
+
* Carries a `data-start` on purpose. A real bus has none — its automation clock
|
|
199
|
+
* is composition time — but the timing gate has to refuse the TAG rather than
|
|
200
|
+
* merely fall out of a missing attribute, or something writing one would put
|
|
201
|
+
* Start/Duration back on a thing that has no range.
|
|
202
|
+
*/
|
|
203
|
+
function audioBusElement() {
|
|
204
|
+
const element = document.createElement("hf-audio-group");
|
|
205
|
+
return {
|
|
206
|
+
...baseElement(),
|
|
207
|
+
element,
|
|
208
|
+
id: "voiceover",
|
|
209
|
+
selector: "#voiceover",
|
|
210
|
+
label: "Voiceover",
|
|
211
|
+
tagName: "hf-audio-group",
|
|
212
|
+
textFields: [],
|
|
213
|
+
dataAttributes: { start: "0", duration: "8" },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
180
217
|
const INFERRED_TIMING_ANIMATION = {
|
|
181
218
|
id: "a1",
|
|
182
219
|
targetSelector: "#inferred-anim",
|
|
@@ -196,7 +233,7 @@ const INFERRED_TIMING_ANIMATION = {
|
|
|
196
233
|
|
|
197
234
|
async function renderPanel(
|
|
198
235
|
flatEnabled: boolean,
|
|
199
|
-
elementOverride:
|
|
236
|
+
elementOverride: NonNullable<PropertyPanelProps["element"]> = baseElement(),
|
|
200
237
|
propsOverride: Partial<PropertyPanelProps> = {},
|
|
201
238
|
currentTime?: number,
|
|
202
239
|
) {
|
|
@@ -952,3 +989,64 @@ describe("PropertyPanel — flat group entrance animation scoping (fix round)",
|
|
|
952
989
|
RENDER_TIMEOUT_MS,
|
|
953
990
|
);
|
|
954
991
|
});
|
|
992
|
+
|
|
993
|
+
describe("PropertyPanel — Motion is for things that move", () => {
|
|
994
|
+
it.each([
|
|
995
|
+
["a custom music tag", () => document.createElement("music")],
|
|
996
|
+
[
|
|
997
|
+
"an element with an audio source",
|
|
998
|
+
() => {
|
|
999
|
+
const element = document.createElement("div");
|
|
1000
|
+
element.setAttribute("src", "voiceover.mp3");
|
|
1001
|
+
return element;
|
|
1002
|
+
},
|
|
1003
|
+
],
|
|
1004
|
+
])("recognizes %s through the shared audio predicate", async (_label, makeElement) => {
|
|
1005
|
+
const fixture = {
|
|
1006
|
+
...audioClipElement(),
|
|
1007
|
+
element: makeElement(),
|
|
1008
|
+
tagName: "div",
|
|
1009
|
+
};
|
|
1010
|
+
const { host, root } = await renderPanel(true, fixture);
|
|
1011
|
+
const titles = Array.from(
|
|
1012
|
+
host.querySelectorAll<HTMLElement>("[data-flat-group-collapsed], [data-flat-group-open]"),
|
|
1013
|
+
).map((node) => node.textContent ?? "");
|
|
1014
|
+
expect(titles.some((title) => title.includes("Motion"))).toBe(false);
|
|
1015
|
+
expect(titles.some((title) => title.includes("Timing"))).toBe(true);
|
|
1016
|
+
act(() => root.unmount());
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it(
|
|
1020
|
+
"calls the section Timing on an audio clip, and offers no tween editor",
|
|
1021
|
+
async () => {
|
|
1022
|
+
const { host, root } = await renderPanel(true, audioClipElement());
|
|
1023
|
+
const titles = Array.from(
|
|
1024
|
+
host.querySelectorAll<HTMLElement>("[data-flat-group-collapsed], [data-flat-group-open]"),
|
|
1025
|
+
).map((el) => el.textContent ?? "");
|
|
1026
|
+
// The clip's placement survives — it is still a clip on a track.
|
|
1027
|
+
expect(titles.some((t) => t.includes("Timing"))).toBe(true);
|
|
1028
|
+
// "Motion" named the tween editor, which an <audio> element has no
|
|
1029
|
+
// transform, opacity or box for. Showing it was the panel gating on
|
|
1030
|
+
// handler presence rather than on the element.
|
|
1031
|
+
expect(titles.some((t) => t.includes("Motion"))).toBe(false);
|
|
1032
|
+
act(() => root.unmount());
|
|
1033
|
+
},
|
|
1034
|
+
RENDER_TIMEOUT_MS,
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
it(
|
|
1038
|
+
"offers a bus neither — it has no clip range to edit",
|
|
1039
|
+
async () => {
|
|
1040
|
+
const { host, root } = await renderPanel(true, audioBusElement());
|
|
1041
|
+
const titles = Array.from(
|
|
1042
|
+
host.querySelectorAll<HTMLElement>("[data-flat-group-collapsed], [data-flat-group-open]"),
|
|
1043
|
+
).map((el) => el.textContent ?? "");
|
|
1044
|
+
expect(titles.some((t) => t.includes("Motion"))).toBe(false);
|
|
1045
|
+
expect(titles.some((t) => t.includes("Timing"))).toBe(false);
|
|
1046
|
+
// It is still a mixer bus: the reason to select one at all.
|
|
1047
|
+
expect(titles.some((t) => t.includes("Audio FX"))).toBe(true);
|
|
1048
|
+
act(() => root.unmount());
|
|
1049
|
+
},
|
|
1050
|
+
RENDER_TIMEOUT_MS,
|
|
1051
|
+
);
|
|
1052
|
+
});
|
|
@@ -36,6 +36,7 @@ import { type PropertyPanelProps } from "./propertyPanelHelpers";
|
|
|
36
36
|
import { GestureRecordPanelButton } from "./GestureRecordControl";
|
|
37
37
|
import { PropertyPanelEmptyState } from "./PropertyPanelEmptyState";
|
|
38
38
|
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
|
39
|
+
import { isAudioDomElement } from "../../utils/timelineInspector";
|
|
39
40
|
|
|
40
41
|
// Re-export helpers that external consumers import from this module
|
|
41
42
|
export {
|
|
@@ -119,6 +120,19 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
119
120
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
120
121
|
const selectedElementHidden = isSelectedElementHidden(timelineElements, selectedElementId);
|
|
121
122
|
const visibilityToggleLabel = selectedElementHidden ? "Show element" : "Hide element";
|
|
123
|
+
/**
|
|
124
|
+
* An audio element gets no hide control here.
|
|
125
|
+
*
|
|
126
|
+
* On an audio track "hidden" and "muted" are not similar operations, they are
|
|
127
|
+
* the SAME operation with two names (groups doc §2.1) — which is why the
|
|
128
|
+
* timeline's eye became the mute rather than growing a sibling. A second copy
|
|
129
|
+
* in the panel, still called "Hide element", is precisely the thing that step
|
|
130
|
+
* removed: "Two controls that silence a track, sitting next to each other,
|
|
131
|
+
* differing only in a distinction the author cannot see." An
|
|
132
|
+
* `<hf-audio-group>` has no visual to hide at all, and its mute lives on its
|
|
133
|
+
* own row.
|
|
134
|
+
*/
|
|
135
|
+
const audioSelection = isAudioDomElement(element?.element);
|
|
122
136
|
// Live during playback, the store's when paused — see the hook. Shared with the
|
|
123
137
|
// audio FX panel, which follows the playhead for the same reason: a value the
|
|
124
138
|
// timeline drives has to be shown moving, not frozen at what the attribute says.
|
|
@@ -309,7 +323,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
309
323
|
selectedElementId={selectedElementId}
|
|
310
324
|
selectedElementHidden={selectedElementHidden}
|
|
311
325
|
visibilityLabel={visibilityToggleLabel}
|
|
312
|
-
onToggleHidden={onToggleElementHidden}
|
|
326
|
+
onToggleHidden={audioSelection ? undefined : onToggleElementHidden}
|
|
313
327
|
/>
|
|
314
328
|
</div>
|
|
315
329
|
</div>
|
|
@@ -71,4 +71,78 @@ describe("PropertyPanelEmptyState — flat multi-select", () => {
|
|
|
71
71
|
expect(onClearSelection).toHaveBeenCalledTimes(1);
|
|
72
72
|
act(() => root.unmount());
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
// A layout group is a positioned wrapper around a bounding box; an <audio>
|
|
76
|
+
// clip has none (offsetWidth/Height are 0), so grouping audio produced a 0x0
|
|
77
|
+
// div with inline left/top on elements that are never laid out. Withheld
|
|
78
|
+
// rather than offered-then-refused.
|
|
79
|
+
const audioElements = (tags: string[]) =>
|
|
80
|
+
tags.map((tag, i) => ({
|
|
81
|
+
id: `el-${i}`,
|
|
82
|
+
selector: `#el-${i}`,
|
|
83
|
+
label: `El ${i}`,
|
|
84
|
+
tagName: tag,
|
|
85
|
+
element: document.createElement(tag),
|
|
86
|
+
})) as unknown as DomEditSelection[];
|
|
87
|
+
|
|
88
|
+
it("withholds both actions when the selection includes audio", () => {
|
|
89
|
+
const { host, root } = renderInto(
|
|
90
|
+
<PropertyPanelEmptyState
|
|
91
|
+
flat
|
|
92
|
+
multiSelectCount={2}
|
|
93
|
+
multiSelectedElements={audioElements(["audio", "audio"])}
|
|
94
|
+
onGroupSelection={vi.fn()}
|
|
95
|
+
onHideAllSelected={vi.fn()}
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
expect(host.querySelector('[data-flat-multiselect-group="true"]')).toBeNull();
|
|
99
|
+
// Hiding is visibility, and `data-hidden` on audio is what MUTES it — the
|
|
100
|
+
// timeline withholds the eye on an audio track for that reason, and this
|
|
101
|
+
// panel was the way back to the same write.
|
|
102
|
+
expect(host.querySelector('[data-flat-multiselect-hide-all="true"]')).toBeNull();
|
|
103
|
+
// The list still names what is selected; only the actions go.
|
|
104
|
+
expect(host.textContent).toContain("2 elements selected");
|
|
105
|
+
act(() => root.unmount());
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("withholds it for a mixed selection too, since the wrapper would still take audio in", () => {
|
|
109
|
+
const { host, root } = renderInto(
|
|
110
|
+
<PropertyPanelEmptyState
|
|
111
|
+
flat
|
|
112
|
+
multiSelectCount={2}
|
|
113
|
+
multiSelectedElements={audioElements(["div", "audio"])}
|
|
114
|
+
onGroupSelection={vi.fn()}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
expect(host.querySelector('[data-flat-multiselect-group="true"]')).toBeNull();
|
|
118
|
+
expect(host.querySelector('[data-flat-multiselect-hide-all="true"]')).toBeNull();
|
|
119
|
+
act(() => root.unmount());
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("counts an <hf-audio-group> bus as audio too", () => {
|
|
123
|
+
const { host, root } = renderInto(
|
|
124
|
+
<PropertyPanelEmptyState
|
|
125
|
+
flat
|
|
126
|
+
multiSelectCount={2}
|
|
127
|
+
multiSelectedElements={audioElements(["hf-audio-group", "div"])}
|
|
128
|
+
onGroupSelection={vi.fn()}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
expect(host.querySelector('[data-flat-multiselect-group="true"]')).toBeNull();
|
|
132
|
+
act(() => root.unmount());
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("still offers both for a selection of layout elements", () => {
|
|
136
|
+
const { host, root } = renderInto(
|
|
137
|
+
<PropertyPanelEmptyState
|
|
138
|
+
flat
|
|
139
|
+
multiSelectCount={2}
|
|
140
|
+
multiSelectedElements={audioElements(["div", "span"])}
|
|
141
|
+
onGroupSelection={vi.fn()}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
expect(host.querySelector('[data-flat-multiselect-group="true"]')).not.toBeNull();
|
|
145
|
+
expect(host.querySelector('[data-flat-multiselect-hide-all="true"]')).not.toBeNull();
|
|
146
|
+
act(() => root.unmount());
|
|
147
|
+
});
|
|
74
148
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Eye, Layers } from "../../icons/SystemIcons";
|
|
2
2
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
3
|
+
import { canHideSelections } from "../../utils/timelineInspector";
|
|
3
4
|
|
|
4
5
|
function FlatEmptyState() {
|
|
5
6
|
return (
|
|
@@ -67,6 +68,9 @@ function FlatMultiSelectState({
|
|
|
67
68
|
onHideAllSelected?: () => void;
|
|
68
69
|
onClearSelection?: () => void;
|
|
69
70
|
}) {
|
|
71
|
+
// One predicate for both actions and for the handler's own refusal, so the
|
|
72
|
+
// button and the refusal cannot disagree about what audio is.
|
|
73
|
+
const hasAudio = !canHideSelections(multiSelectedElements);
|
|
70
74
|
return (
|
|
71
75
|
<div className="flex flex-col gap-3 px-4 py-3">
|
|
72
76
|
<div className="flex items-center gap-3 rounded-xl border border-panel-border bg-panel-surface p-3">
|
|
@@ -123,26 +127,37 @@ function FlatMultiSelectState({
|
|
|
123
127
|
);
|
|
124
128
|
})}
|
|
125
129
|
</div>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
130
|
+
{/* Neither action applies to audio, so the row goes rather than showing
|
|
131
|
+
an empty frame. Grouping is the LAYOUT grouper — a positioned wrapper
|
|
132
|
+
around a bounding box, and an <audio> clip has none (grouping two
|
|
133
|
+
produced a 0x0 div with inline left/top on elements that are never
|
|
134
|
+
laid out). Hiding is visibility, which for audio doubles as mute; the
|
|
135
|
+
timeline already withholds the eye on an audio track
|
|
136
|
+
(`visible={!isAudioTrack}`) and this panel was the way back to the
|
|
137
|
+
same write. Both handlers refuse it too — they own keyboard paths no
|
|
138
|
+
hidden button can gate. */}
|
|
139
|
+
{!hasAudio && (
|
|
140
|
+
<div className="flex gap-2">
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
data-flat-multiselect-group="true"
|
|
144
|
+
onClick={onGroupSelection}
|
|
145
|
+
className="flex h-[34px] flex-1 items-center justify-center gap-2 rounded-lg bg-panel-hover text-[11px] font-semibold text-panel-text-0"
|
|
146
|
+
>
|
|
147
|
+
<Layers size={13} />
|
|
148
|
+
Group selection
|
|
149
|
+
</button>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
data-flat-multiselect-hide-all="true"
|
|
153
|
+
onClick={onHideAllSelected}
|
|
154
|
+
className="flex h-[34px] items-center gap-1.5 rounded-lg border border-panel-border-input bg-panel-input px-3 text-[11px] font-medium text-panel-text-2"
|
|
155
|
+
>
|
|
156
|
+
<Eye size={13} />
|
|
157
|
+
Hide all
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
146
161
|
<span className="text-center text-[10px] text-panel-text-5">
|
|
147
162
|
Select a single element to edit its properties
|
|
148
163
|
</span>
|