@hyperframes/studio 0.8.79 → 0.8.81
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-C68UawR3.js +464 -0
- package/dist/assets/{index-ORZXYB0k.js → index-CMzPGv9P.js} +1 -1
- package/dist/assets/{index-CJP92F4-.js → index-CS5SImRT.js} +1 -1
- package/dist/assets/{index-DBKySIo7.js → index-hNVM9Pd2.js} +200 -197
- package/dist/index.d.ts +18 -1
- package/dist/index.html +1 -1
- package/dist/index.js +1673 -1348
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/App.tsx +13 -10
- package/src/components/StudioRightPanels.tsx +4 -48
- package/src/components/editor/TimelineFxPopover.test.tsx +1 -1
- package/src/components/editor/TimelineFxPopover.tsx +1 -1
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsParsing.ts +4 -0
- package/src/components/editor/useFxAudition.ts +3 -3
- package/src/components/nle/useTimelineEditCallbacks.ts +4 -0
- package/src/components/panels/VariablesOtherCompositions.tsx +4 -4
- package/src/components/panels/VariablesPanel.test.tsx +86 -0
- package/src/components/panels/VariablesPanel.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.test.tsx +29 -0
- package/src/components/sidebar/SidebarLintButton.tsx +4 -0
- package/src/components/studioColorGradingScope.ts +5 -7
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/TimelineEditContext.tsx +2 -0
- package/src/hooks/liveLanes.ts +147 -0
- package/src/hooks/timelineAudioGroupVolume.ts +81 -47
- package/src/hooks/timelineEditPermission.ts +9 -1
- package/src/hooks/timelineEditingHelpers.test.ts +16 -0
- package/src/hooks/timelineEditingHelpers.ts +69 -43
- package/src/hooks/timelineElementFxAttribute.ts +82 -36
- package/src/hooks/useAppHotkeys.ts +1 -1
- package/src/hooks/useApplyColorGradingScope.test.tsx +53 -0
- package/src/hooks/useApplyColorGradingScope.ts +54 -0
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +125 -21
- package/src/hooks/useAutomationSelectionKeyboard.ts +21 -25
- package/src/hooks/useBlockHandlers.test.tsx +65 -0
- package/src/hooks/useBlockHandlers.ts +7 -4
- package/src/hooks/useDomEditAttributeCommits.ts +3 -17
- package/src/hooks/useDomEditSession.ts +1 -0
- package/src/hooks/useEditHistoryActions.ts +4 -3
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +49 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +15 -6
- package/src/hooks/useLintModal.test.tsx +15 -0
- package/src/hooks/useLintModal.ts +5 -2
- package/src/hooks/useProjectCompositionVariables.ts +3 -3
- package/src/hooks/useTimelineEditing.ts +19 -16
- package/src/hooks/useTimelineGroupEditing.ts +2 -1
- package/src/hooks/useToast.ts +2 -1
- package/src/player/components/ImageThumbnail.tsx +3 -12
- package/src/player/components/Player.test.ts +23 -0
- package/src/player/components/Player.tsx +4 -2
- package/src/player/components/TimelineAutomationLane.test.tsx +38 -0
- package/src/player/components/TimelineAutomationLane.tsx +18 -13
- package/src/player/components/TimelineAutomationLaneSlot.test.tsx +0 -2
- package/src/player/components/TimelineClipFades.test.tsx +24 -3
- package/src/player/components/TimelineClipFades.tsx +5 -2
- package/src/player/components/TimelineFxButton.tsx +1 -1
- package/src/player/components/TimelineGroupHeader.tsx +1 -1
- package/src/player/components/TimelineGroupRow.test.tsx +0 -1
- package/src/player/components/TimelineGroupRow.tsx +17 -11
- package/src/player/components/TimelineTrackHeader.tsx +21 -6
- package/src/player/components/groupAutomationElement.ts +9 -1
- package/src/player/components/timelineCallbacks.ts +2 -0
- package/src/player/components/timelineClipDragPreview.test.ts +41 -0
- package/src/player/components/timelineClipDragPreview.ts +15 -24
- package/src/player/components/timelineEditing.test.ts +99 -1
- package/src/player/components/timelineEditing.ts +5 -8
- package/src/player/components/timelineGroupEditing.test.ts +27 -0
- package/src/player/components/timelineGroupEditing.ts +18 -15
- package/src/player/components/timelineStackingSync.ts +5 -5
- package/src/player/components/timelineZones.ts +2 -3
- package/src/player/components/useAutomationEdgeStretch.ts +4 -4
- package/src/player/components/useAutomationLaneGestures.ts +1 -1
- package/src/player/components/useAutomationLanes.save.test.tsx +1406 -0
- package/src/player/components/useAutomationLanes.ts +58 -74
- package/src/player/lib/automationStoreSync.ts +30 -3
- package/src/player/lib/thumbnailImageDecoder.test.ts +82 -0
- package/src/player/lib/thumbnailImageDecoder.ts +73 -0
- package/src/player/lib/timelineDOM.test.ts +28 -8
- package/src/player/lib/timelineElementHelpers.ts +3 -4
- package/src/player/lib/timelineGroupInfo.ts +23 -23
- package/src/utils/studioFileHistory.ts +36 -30
- package/dist/assets/hyperframes-player-Bx_Kw4Ey.js +0 -464
- package/src/player/components/automationGestureKeys.test.ts +0 -49
- package/src/player/components/automationGestureKeys.ts +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.81",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"dockview-react": "8.3.1",
|
|
53
53
|
"gsap": "^3.13.0",
|
|
54
54
|
"mediabunny": "^1.45.3",
|
|
55
|
-
"@hyperframes/core": "0.8.
|
|
56
|
-
"@hyperframes/parsers": "0.8.
|
|
57
|
-
"@hyperframes/
|
|
58
|
-
"@hyperframes/
|
|
59
|
-
"@hyperframes/shader-transitions": "0.8.
|
|
60
|
-
"@hyperframes/studio-server": "0.8.
|
|
55
|
+
"@hyperframes/core": "0.8.81",
|
|
56
|
+
"@hyperframes/parsers": "0.8.81",
|
|
57
|
+
"@hyperframes/player": "0.8.81",
|
|
58
|
+
"@hyperframes/sdk": "0.8.81",
|
|
59
|
+
"@hyperframes/shader-transitions": "0.8.81",
|
|
60
|
+
"@hyperframes/studio-server": "0.8.81"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"vite": "^6.4.2",
|
|
74
74
|
"vitest": "^4.1.11",
|
|
75
75
|
"zustand": "^5.0.0",
|
|
76
|
-
"@hyperframes/producer": "0.8.
|
|
76
|
+
"@hyperframes/producer": "0.8.81"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
79
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -209,6 +209,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
209
209
|
refreshFileTree: fileManager.refreshFileTree,
|
|
210
210
|
reloadPreview,
|
|
211
211
|
showToast,
|
|
212
|
+
dismissToast,
|
|
212
213
|
},
|
|
213
214
|
previewIframeRef,
|
|
214
215
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
@@ -260,7 +261,10 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
260
261
|
handleDuplicate,
|
|
261
262
|
onResetKeyframes: () => resetKeyframesRef.current(),
|
|
262
263
|
onDeleteSelectedKeyframes: () => deleteSelectedKeyframesRef.current(),
|
|
263
|
-
onAfterUndoRedo: () =>
|
|
264
|
+
onAfterUndoRedo: (restore) => {
|
|
265
|
+
invalidateGsapCacheRef.current();
|
|
266
|
+
timelineEditing.restoreLiveLanes(restore);
|
|
267
|
+
},
|
|
264
268
|
onGroupSelection: () => domEditSessionRef.current.handleGroupSelection(),
|
|
265
269
|
onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
|
|
266
270
|
activeCompPath,
|
|
@@ -340,8 +344,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
340
344
|
effectiveTimelineDuration,
|
|
341
345
|
});
|
|
342
346
|
const compositionDimensions = useCompositionDimensions(previewIframeRef);
|
|
343
|
-
const
|
|
344
|
-
useLintModal(projectId, refreshKey);
|
|
347
|
+
const lint = useLintModal(projectId, refreshKey);
|
|
345
348
|
const frameCapture = useFrameCapture({
|
|
346
349
|
projectId,
|
|
347
350
|
activeCompPath,
|
|
@@ -496,11 +499,11 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
496
499
|
onSelectComposition={handleSelectComposition}
|
|
497
500
|
onAddBlock={handleAddBlock}
|
|
498
501
|
onPreviewBlock={setBlockPreview}
|
|
499
|
-
onLint={handleLint}
|
|
500
|
-
linting={linting}
|
|
501
|
-
lintFindingCount={
|
|
502
|
-
lintFindingsByFile={findingsByFile}
|
|
503
|
-
lintHasError={hasLintError}
|
|
502
|
+
onLint={lint.handleLint}
|
|
503
|
+
linting={lint.linting}
|
|
504
|
+
lintFindingCount={lint.lintFindingCount}
|
|
505
|
+
lintFindingsByFile={lint.findingsByFile}
|
|
506
|
+
lintHasError={lint.hasLintError}
|
|
504
507
|
onAddAssetToTimeline={handleAddAssetAtPlayhead}
|
|
505
508
|
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
|
|
506
509
|
/>
|
|
@@ -573,8 +576,8 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
573
576
|
<StudioOverlays
|
|
574
577
|
projectId={projectId}
|
|
575
578
|
projectDir={fileManager.projectDir}
|
|
576
|
-
lintModal={lintModal}
|
|
577
|
-
closeLintModal={closeLintModal}
|
|
579
|
+
lintModal={lint.lintModal}
|
|
580
|
+
closeLintModal={lint.closeLintModal}
|
|
578
581
|
consoleErrors={consoleErrors}
|
|
579
582
|
clearConsoleErrors={() => setConsoleErrors(null)}
|
|
580
583
|
domEditSession={domEditSession}
|
|
@@ -23,14 +23,10 @@ import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/Stu
|
|
|
23
23
|
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
24
24
|
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
25
25
|
import { usePlayerStore } from "../player";
|
|
26
|
-
import {
|
|
27
|
-
applyColorGradingScopeUpdate,
|
|
28
|
-
EMPTY_COLOR_GRADING_SCOPE_RESULT,
|
|
29
|
-
type ColorGradingScope,
|
|
30
|
-
} from "./studioColorGradingScope";
|
|
31
26
|
import { timelineKeysForSelections } from "../utils/studioHelpers";
|
|
32
27
|
import { canHideSelections } from "../utils/timelineInspector";
|
|
33
28
|
import { useRemoveBackground } from "../hooks/useRemoveBackground";
|
|
29
|
+
import { useApplyColorGradingScope } from "../hooks/useApplyColorGradingScope";
|
|
34
30
|
|
|
35
31
|
// fallow-ignore-next-line complexity
|
|
36
32
|
export function StudioRightPanels({
|
|
@@ -49,14 +45,8 @@ export function StudioRightPanels({
|
|
|
49
45
|
onAutoGroupCarveSources,
|
|
50
46
|
onAddMediaOverlay,
|
|
51
47
|
}: StudioRightPanelsProps) {
|
|
52
|
-
const {
|
|
53
|
-
|
|
54
|
-
projectId,
|
|
55
|
-
activeCompPath,
|
|
56
|
-
showToast,
|
|
57
|
-
waitForPendingDomEditSaves,
|
|
58
|
-
renderQueue,
|
|
59
|
-
} = useStudioShellContext();
|
|
48
|
+
const { previewIframeRef, projectId, activeCompPath, showToast, renderQueue } =
|
|
49
|
+
useStudioShellContext();
|
|
60
50
|
const { captionEditMode, refreshKey } = useStudioPlaybackContext();
|
|
61
51
|
|
|
62
52
|
const {
|
|
@@ -114,7 +104,6 @@ export function StudioRightPanels({
|
|
|
114
104
|
refreshFileTree,
|
|
115
105
|
readProjectFile,
|
|
116
106
|
writeProjectFile,
|
|
117
|
-
fileTree,
|
|
118
107
|
editingFile,
|
|
119
108
|
} = useFileManagerContext();
|
|
120
109
|
|
|
@@ -158,40 +147,7 @@ export function StudioRightPanels({
|
|
|
158
147
|
});
|
|
159
148
|
useCaptionDesignFocus(captionEditMode);
|
|
160
149
|
|
|
161
|
-
const handleApplyColorGradingScope =
|
|
162
|
-
async (scope: ColorGradingScope, value: string | null) =>
|
|
163
|
-
applyColorGradingScopeUpdate({
|
|
164
|
-
scope,
|
|
165
|
-
value,
|
|
166
|
-
selectedSourceFile: domEditSelection?.sourceFile || activeCompPath || "index.html",
|
|
167
|
-
fileTree,
|
|
168
|
-
projectId,
|
|
169
|
-
waitForPendingDomEditSaves,
|
|
170
|
-
readProjectFile,
|
|
171
|
-
writeProjectFile,
|
|
172
|
-
recordEdit,
|
|
173
|
-
reloadPreview,
|
|
174
|
-
showToast,
|
|
175
|
-
}).catch((error) => {
|
|
176
|
-
showToast(
|
|
177
|
-
`Couldn't apply color grading: ${error instanceof Error ? error.message : String(error)}`,
|
|
178
|
-
"error",
|
|
179
|
-
);
|
|
180
|
-
return EMPTY_COLOR_GRADING_SCOPE_RESULT;
|
|
181
|
-
}),
|
|
182
|
-
[
|
|
183
|
-
activeCompPath,
|
|
184
|
-
domEditSelection?.sourceFile,
|
|
185
|
-
fileTree,
|
|
186
|
-
projectId,
|
|
187
|
-
readProjectFile,
|
|
188
|
-
recordEdit,
|
|
189
|
-
reloadPreview,
|
|
190
|
-
showToast,
|
|
191
|
-
waitForPendingDomEditSaves,
|
|
192
|
-
writeProjectFile,
|
|
193
|
-
],
|
|
194
|
-
);
|
|
150
|
+
const handleApplyColorGradingScope = useApplyColorGradingScope(recordEdit, reloadPreview);
|
|
195
151
|
|
|
196
152
|
const handleRemoveBackground = useRemoveBackground(projectId, refreshFileTree, showToast);
|
|
197
153
|
|
|
@@ -87,7 +87,7 @@ describe("TimelineFxPopover", () => {
|
|
|
87
87
|
act(() => {
|
|
88
88
|
button?.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
|
|
89
89
|
});
|
|
90
|
-
expect(onChainPreview).toHaveBeenCalledWith(EMPTY_CHAIN);
|
|
90
|
+
expect(onChainPreview).toHaveBeenCalledWith(EMPTY_CHAIN, true);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
it("Escape closes without letting the keystroke propagate past the popover", () => {
|
|
@@ -75,7 +75,7 @@ export interface TimelineFxPopoverProps {
|
|
|
75
75
|
* selected clip's attribute — the caller resolves which). */
|
|
76
76
|
onChainChange: (next: HfAudioFxChain) => void;
|
|
77
77
|
/** Preview a hypothetical chain on the running graph without persisting. */
|
|
78
|
-
onChainPreview?: (next: HfAudioFxChain) => void;
|
|
78
|
+
onChainPreview?: (next: HfAudioFxChain, ended?: boolean) => void;
|
|
79
79
|
onAuditionTransport?: (on: boolean) => void;
|
|
80
80
|
/** Select the target the way clicking it in the timeline does, and ensure
|
|
81
81
|
* the property panel's Audio FX group is expanded. */
|
|
@@ -34,6 +34,10 @@ export function readStudioFileChangePath(payload: unknown): string | null {
|
|
|
34
34
|
return path === null ? null : normalizeStudioFileChangePath(path);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function readFileChangeAffectsPreview(payload: unknown): boolean {
|
|
38
|
+
return asPayloadRecord(payload)?.affectsPreview !== false;
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
/**
|
|
38
42
|
* The compositions whose thumbnails a change can alter, or `null` for all of them. Anything
|
|
39
43
|
* but a list of paths (an older server, the Vite dev host) reads as "all".
|
|
@@ -11,7 +11,7 @@ import type { HfAudioFxChain } from "@hyperframes/core/audio-fx";
|
|
|
11
11
|
|
|
12
12
|
export function useFxAudition(
|
|
13
13
|
chain: HfAudioFxChain,
|
|
14
|
-
onChainPreview: ((chain: HfAudioFxChain) => void) | undefined,
|
|
14
|
+
onChainPreview: ((chain: HfAudioFxChain, ended?: boolean) => void) | undefined,
|
|
15
15
|
onAuditionTransport: ((on: boolean) => void) | undefined,
|
|
16
16
|
) {
|
|
17
17
|
/**
|
|
@@ -45,7 +45,7 @@ export function useFxAudition(
|
|
|
45
45
|
// Stop before reverting, for the mirror of that reason — the last thing
|
|
46
46
|
// heard should be the preset, not a frame of the chain coming back.
|
|
47
47
|
onAuditionTransport?.(false);
|
|
48
|
-
onChainPreview(auditionBase.current);
|
|
48
|
+
onChainPreview(auditionBase.current, true);
|
|
49
49
|
auditionBase.current = null;
|
|
50
50
|
}
|
|
51
51
|
},
|
|
@@ -96,7 +96,7 @@ export function useFxAudition(
|
|
|
96
96
|
() => () => {
|
|
97
97
|
if (auditionBase.current) {
|
|
98
98
|
transportRef.current?.(false);
|
|
99
|
-
previewRef.current?.(auditionBase.current);
|
|
99
|
+
previewRef.current?.(auditionBase.current, true);
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
[],
|
|
@@ -41,6 +41,7 @@ export interface TimelineEditCallbackDeps {
|
|
|
41
41
|
handleToggleTrackHidden: (track: number, hidden: boolean) => Promise<void> | void;
|
|
42
42
|
setAudioGroupAttribute: {
|
|
43
43
|
setLive: (groupId: string, attr: string, value: string | null) => void;
|
|
44
|
+
revertLive?: (groupId: string, attr: string) => void;
|
|
44
45
|
setQuiet: (
|
|
45
46
|
groupId: string,
|
|
46
47
|
attr: string,
|
|
@@ -61,6 +62,7 @@ export interface TimelineEditCallbackDeps {
|
|
|
61
62
|
/** C1's single-clip FX write, addressed by the clip itself. */
|
|
62
63
|
setElementFxAttribute?: {
|
|
63
64
|
setLive: (element: TimelineElement, attr: string, value: string | null) => void;
|
|
65
|
+
revertLive?: (element: TimelineElement, attr: string) => void;
|
|
64
66
|
setQuiet: (
|
|
65
67
|
element: TimelineElement,
|
|
66
68
|
attr: string,
|
|
@@ -216,9 +218,11 @@ export function useTimelineEditCallbacks({
|
|
|
216
218
|
onToggleTrackHidden: handleToggleTrackHidden,
|
|
217
219
|
onSetAudioGroupAttributeLive: setAudioGroupAttribute.setLive,
|
|
218
220
|
onSetAudioGroupAttributeQuiet: setAudioGroupAttribute.setQuiet,
|
|
221
|
+
onRevertAudioGroupAttributeLive: setAudioGroupAttribute.revertLive,
|
|
219
222
|
onGroupClips: handleGroupClips,
|
|
220
223
|
onSetElementAttributeLive: setElementFxAttribute?.setLive,
|
|
221
224
|
onSetElementAttributeQuiet: setElementFxAttribute?.setQuiet,
|
|
225
|
+
onRevertElementAttributeLive: setElementFxAttribute?.revertLive,
|
|
222
226
|
onBlockedEditAttempt: handleBlockedTimelineEdit,
|
|
223
227
|
onSplitElement: handleTimelineElementSplit,
|
|
224
228
|
onRazorSplit: handleRazorSplit,
|
|
@@ -85,7 +85,7 @@ function CompositionSection({
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function VariablesOtherCompositions({
|
|
88
|
-
|
|
88
|
+
compositionPaths,
|
|
89
89
|
excludePath,
|
|
90
90
|
refreshKey,
|
|
91
91
|
readProjectFile,
|
|
@@ -93,8 +93,8 @@ export function VariablesOtherCompositions({
|
|
|
93
93
|
recordEdit,
|
|
94
94
|
reloadPreview,
|
|
95
95
|
}: {
|
|
96
|
-
|
|
97
|
-
excludePath: string;
|
|
96
|
+
compositionPaths: string[];
|
|
97
|
+
excludePath: string | null;
|
|
98
98
|
refreshKey: unknown;
|
|
99
99
|
readProjectFile: (path: string) => Promise<string>;
|
|
100
100
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
@@ -103,7 +103,7 @@ export function VariablesOtherCompositions({
|
|
|
103
103
|
}) {
|
|
104
104
|
const [selfRefresh, setSelfRefresh] = useState(0);
|
|
105
105
|
const groups = useProjectCompositionVariables(
|
|
106
|
-
|
|
106
|
+
compositionPaths,
|
|
107
107
|
excludePath,
|
|
108
108
|
readProjectFile,
|
|
109
109
|
`${refreshKey}:${selfRefresh}`,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { openComposition } from "@hyperframes/sdk";
|
|
7
|
+
import { VariablesPanel } from "./VariablesPanel";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
const withTitle = (id: string, label: string) => `<!DOCTYPE html>
|
|
12
|
+
<html data-composition-variables='[{"id":"title","type":"string","label":"${label}","default":"Hello"}]'>
|
|
13
|
+
<body><div data-composition-id="${id}" data-hf-id="hf-stage" data-hf-root data-duration="5"></div></body>
|
|
14
|
+
</html>`;
|
|
15
|
+
|
|
16
|
+
const files: Record<string, string> = {
|
|
17
|
+
"index.html":
|
|
18
|
+
'<html><body><div data-hf-id="hf-stage" data-hf-root data-duration="5"></div></body></html>',
|
|
19
|
+
"main.html": withTitle("main", "Main title"),
|
|
20
|
+
"scene.html": withTitle("scene", "Scene title"),
|
|
21
|
+
".hyperframes/preview/4884df6e.html": withTitle("root", "Generated title"),
|
|
22
|
+
};
|
|
23
|
+
const shell = {
|
|
24
|
+
activeCompPath: "index.html" as string | null,
|
|
25
|
+
compositions: ["index.html", "scene.html"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
vi.mock("../../contexts/StudioContext", () => ({
|
|
29
|
+
useStudioShellContext: () => ({ activeCompPath: shell.activeCompPath, showToast: vi.fn() }),
|
|
30
|
+
useStudioPlaybackContext: () => ({ refreshKey: 0 }),
|
|
31
|
+
}));
|
|
32
|
+
vi.mock("../../contexts/DomEditContext", () => ({
|
|
33
|
+
useDomEditContext: () => ({ domEditSelection: null }),
|
|
34
|
+
}));
|
|
35
|
+
vi.mock("../../contexts/FileManagerContext", () => ({
|
|
36
|
+
useFileManagerContext: () => ({
|
|
37
|
+
readProjectFile: async (path: string) => files[path] ?? "",
|
|
38
|
+
writeProjectFile: vi.fn(),
|
|
39
|
+
fileTree: Object.keys(files),
|
|
40
|
+
compositions: shell.compositions,
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
let root: Root | null = null;
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
act(() => root?.unmount());
|
|
48
|
+
root = null;
|
|
49
|
+
document.body.innerHTML = "";
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
async function renderOtherCompositions(sessionPath: string): Promise<string> {
|
|
53
|
+
const host = document.createElement("div");
|
|
54
|
+
document.body.append(host);
|
|
55
|
+
root = createRoot(host);
|
|
56
|
+
const session = await openComposition(files[sessionPath]!, { history: false });
|
|
57
|
+
await act(async () => {
|
|
58
|
+
root?.render(
|
|
59
|
+
<VariablesPanel
|
|
60
|
+
sdkSession={session}
|
|
61
|
+
publishSdkSession={vi.fn()}
|
|
62
|
+
reloadPreview={vi.fn()}
|
|
63
|
+
recordEdit={vi.fn(async () => {})}
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
const other = () =>
|
|
68
|
+
[...host.querySelectorAll("p")].find((p) => p.textContent === "Other compositions")
|
|
69
|
+
?.parentElement?.textContent ?? "";
|
|
70
|
+
await vi.waitFor(() => expect(other()).toContain("scene.html"));
|
|
71
|
+
return other();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("VariablesPanel other compositions", () => {
|
|
75
|
+
it("lists the project's compositions, not Studio's generated preview documents", async () => {
|
|
76
|
+
shell.activeCompPath = "index.html";
|
|
77
|
+
shell.compositions = ["index.html", "scene.html"];
|
|
78
|
+
expect(await renderOtherCompositions("index.html")).not.toContain(".hyperframes/preview");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("leaves out the main composition in the master view when it is not index.html", async () => {
|
|
82
|
+
shell.activeCompPath = null;
|
|
83
|
+
shell.compositions = ["main.html", "scene.html"];
|
|
84
|
+
expect(await renderOtherCompositions("main.html")).not.toContain("main.html");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -254,7 +254,7 @@ export const VariablesPanel = memo(function VariablesPanel({
|
|
|
254
254
|
}: VariablesPanelProps) {
|
|
255
255
|
const { activeCompPath, showToast } = useStudioShellContext();
|
|
256
256
|
const { refreshKey } = useStudioPlaybackContext();
|
|
257
|
-
const { readProjectFile, writeProjectFile,
|
|
257
|
+
const { readProjectFile, writeProjectFile, compositions } = useFileManagerContext();
|
|
258
258
|
const { domEditSelection } = useDomEditContext();
|
|
259
259
|
// Master view (no activeCompPath) targets the real main composition, not a
|
|
260
260
|
// hardcoded index.html — used for both the persist write target and the
|
|
@@ -537,8 +537,8 @@ export const VariablesPanel = memo(function VariablesPanel({
|
|
|
537
537
|
</button>
|
|
538
538
|
)}
|
|
539
539
|
<VariablesOtherCompositions
|
|
540
|
-
|
|
541
|
-
excludePath={
|
|
540
|
+
compositionPaths={compositions}
|
|
541
|
+
excludePath={effectiveCompPath}
|
|
542
542
|
refreshKey={`${refreshKey}:${revision}`}
|
|
543
543
|
readProjectFile={readProjectFile}
|
|
544
544
|
writeProjectFile={writeProjectFile}
|
|
@@ -44,6 +44,35 @@ describe("SidebarLintButton badge", () => {
|
|
|
44
44
|
expect(badge?.textContent).toContain("including errors");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it("pulses a few times and then holds still, so an idle Studio paints nothing", () => {
|
|
48
|
+
const badge = renderBadge(true);
|
|
49
|
+
expect(badge?.style.animationIterationCount).toBe("3");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("pulses again when the finding count changes", () => {
|
|
53
|
+
const host = document.createElement("div");
|
|
54
|
+
document.body.append(host);
|
|
55
|
+
root = createRoot(host);
|
|
56
|
+
const render = (findingCount: number) =>
|
|
57
|
+
act(() => {
|
|
58
|
+
root?.render(
|
|
59
|
+
<SidebarLintButton
|
|
60
|
+
onLint={vi.fn()}
|
|
61
|
+
linting={false}
|
|
62
|
+
findingCount={findingCount}
|
|
63
|
+
hasError
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
render(2);
|
|
68
|
+
const first = host.querySelector("[data-lint-badge]");
|
|
69
|
+
render(2);
|
|
70
|
+
expect(host.querySelector("[data-lint-badge]")).toBe(first);
|
|
71
|
+
render(3);
|
|
72
|
+
// A new element restarts its CSS animation; a text change on the old one would not.
|
|
73
|
+
expect(host.querySelector("[data-lint-badge]")).not.toBe(first);
|
|
74
|
+
});
|
|
75
|
+
|
|
47
76
|
it("stays still when the findings are warnings only", () => {
|
|
48
77
|
const badge = renderBadge(false);
|
|
49
78
|
expect(badge?.dataset.lintBadge).toBe("warning");
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const ERROR_PULSES_BEFORE_IDLE = 3;
|
|
2
|
+
|
|
1
3
|
export function SidebarLintButton({
|
|
2
4
|
onLint,
|
|
3
5
|
linting,
|
|
@@ -31,7 +33,9 @@ export function SidebarLintButton({
|
|
|
31
33
|
{linting ? "Linting…" : "Lint"}
|
|
32
34
|
{!linting && findingCount != null && findingCount > 0 && (
|
|
33
35
|
<span
|
|
36
|
+
key={findingCount}
|
|
34
37
|
data-lint-badge={hasError ? "error" : "warning"}
|
|
38
|
+
style={hasError ? { animationIterationCount: ERROR_PULSES_BEFORE_IDLE } : undefined}
|
|
35
39
|
className={
|
|
36
40
|
hasError
|
|
37
41
|
? "ml-1 min-w-[16px] rounded-full bg-panel-danger/25 px-1 text-[9px] font-bold text-panel-danger animate-pulse motion-reduce:animate-none"
|
|
@@ -22,7 +22,7 @@ interface ApplyColorGradingScopeOptions {
|
|
|
22
22
|
scope: ColorGradingScope;
|
|
23
23
|
value: string | null;
|
|
24
24
|
selectedSourceFile: string;
|
|
25
|
-
|
|
25
|
+
compositionPaths: string[];
|
|
26
26
|
projectId: string;
|
|
27
27
|
waitForPendingDomEditSaves: () => Promise<void>;
|
|
28
28
|
readProjectFile: ProjectFileReader;
|
|
@@ -35,11 +35,9 @@ interface ApplyColorGradingScopeOptions {
|
|
|
35
35
|
function colorGradingScopePaths(
|
|
36
36
|
scope: ColorGradingScope,
|
|
37
37
|
selectedSourceFile: string,
|
|
38
|
-
|
|
38
|
+
compositionPaths: string[],
|
|
39
39
|
): string[] {
|
|
40
|
-
return scope === "source-file"
|
|
41
|
-
? [selectedSourceFile]
|
|
42
|
-
: fileTree.filter((path) => /\.html?$/i.test(path));
|
|
40
|
+
return scope === "source-file" ? [selectedSourceFile] : compositionPaths;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
async function patchColorGradingScopeFiles(
|
|
@@ -72,7 +70,7 @@ export async function applyColorGradingScopeUpdate({
|
|
|
72
70
|
scope,
|
|
73
71
|
value,
|
|
74
72
|
selectedSourceFile,
|
|
75
|
-
|
|
73
|
+
compositionPaths,
|
|
76
74
|
projectId,
|
|
77
75
|
waitForPendingDomEditSaves,
|
|
78
76
|
readProjectFile,
|
|
@@ -91,7 +89,7 @@ export async function applyColorGradingScopeUpdate({
|
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
const { files, changedElements } = await patchColorGradingScopeFiles(
|
|
94
|
-
colorGradingScopePaths(scope, selectedSourceFile,
|
|
92
|
+
colorGradingScopePaths(scope, selectedSourceFile, compositionPaths),
|
|
95
93
|
value,
|
|
96
94
|
readProjectFile,
|
|
97
95
|
);
|
|
@@ -13,6 +13,7 @@ export interface DomEditActionsValue extends Pick<
|
|
|
13
13
|
| "handlePreviewCanvasPointerLeave"
|
|
14
14
|
| "applyDomSelection"
|
|
15
15
|
| "clearDomSelection"
|
|
16
|
+
| "refreshDomEditSelectionFromPreview"
|
|
16
17
|
| "handleDomStyleCommit"
|
|
17
18
|
| "handleDomStyleCommitForSelection"
|
|
18
19
|
| "handleDomAttributeCommit"
|
|
@@ -155,6 +156,7 @@ export function DomEditProvider({
|
|
|
155
156
|
handlePreviewCanvasPointerLeave,
|
|
156
157
|
applyDomSelection,
|
|
157
158
|
clearDomSelection,
|
|
159
|
+
refreshDomEditSelectionFromPreview,
|
|
158
160
|
handleDomStyleCommit,
|
|
159
161
|
handleDomStyleCommitForSelection,
|
|
160
162
|
handleDomAttributeCommit,
|
|
@@ -248,6 +250,7 @@ export function DomEditProvider({
|
|
|
248
250
|
handlePreviewCanvasPointerLeave,
|
|
249
251
|
applyDomSelection,
|
|
250
252
|
clearDomSelection,
|
|
253
|
+
refreshDomEditSelectionFromPreview,
|
|
251
254
|
handleDomStyleCommit,
|
|
252
255
|
handleDomStyleCommitForSelection,
|
|
253
256
|
handleDomAttributeCommit,
|
|
@@ -322,6 +325,7 @@ export function DomEditProvider({
|
|
|
322
325
|
handlePreviewCanvasPointerLeave,
|
|
323
326
|
applyDomSelection,
|
|
324
327
|
clearDomSelection,
|
|
328
|
+
refreshDomEditSelectionFromPreview,
|
|
325
329
|
handleDomStyleCommit,
|
|
326
330
|
handleDomStyleCommitForSelection,
|
|
327
331
|
handleDomAttributeCommit,
|
|
@@ -44,9 +44,11 @@ export function TimelineEditProvider({
|
|
|
44
44
|
value.onToggleTrackHidden,
|
|
45
45
|
value.onSetAudioGroupAttributeLive,
|
|
46
46
|
value.onSetAudioGroupAttributeQuiet,
|
|
47
|
+
value.onRevertAudioGroupAttributeLive,
|
|
47
48
|
value.onGroupClips,
|
|
48
49
|
value.onSetElementAttributeLive,
|
|
49
50
|
value.onSetElementAttributeQuiet,
|
|
51
|
+
value.onRevertElementAttributeLive,
|
|
50
52
|
value.onBlockedEditAttempt,
|
|
51
53
|
value.onSplitElement,
|
|
52
54
|
value.onRazorSplit,
|