@hyperframes/studio 0.8.80 → 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-CgnoxDoD.js → hyperframes-player-C68UawR3.js} +1 -1
- package/dist/assets/{index-J9HXe7mU.js → index-CMzPGv9P.js} +1 -1
- package/dist/assets/{index-DZGVvqDT.js → index-CS5SImRT.js} +1 -1
- package/dist/assets/{index-CsG24Jub.js → index-hNVM9Pd2.js} +168 -168
- package/dist/index.html +1 -1
- package/dist/index.js +186 -175
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/App.tsx +8 -9
- package/src/components/StudioRightPanels.tsx +4 -48
- 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/hooks/useApplyColorGradingScope.test.tsx +53 -0
- package/src/hooks/useApplyColorGradingScope.ts +54 -0
- package/src/hooks/useLintModal.test.tsx +15 -0
- package/src/hooks/useLintModal.ts +5 -2
- package/src/hooks/useProjectCompositionVariables.ts +3 -3
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/player": "0.8.
|
|
58
|
-
"@hyperframes/sdk": "0.8.
|
|
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
|
@@ -344,8 +344,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
344
344
|
effectiveTimelineDuration,
|
|
345
345
|
});
|
|
346
346
|
const compositionDimensions = useCompositionDimensions(previewIframeRef);
|
|
347
|
-
const
|
|
348
|
-
useLintModal(projectId, refreshKey);
|
|
347
|
+
const lint = useLintModal(projectId, refreshKey);
|
|
349
348
|
const frameCapture = useFrameCapture({
|
|
350
349
|
projectId,
|
|
351
350
|
activeCompPath,
|
|
@@ -500,11 +499,11 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
500
499
|
onSelectComposition={handleSelectComposition}
|
|
501
500
|
onAddBlock={handleAddBlock}
|
|
502
501
|
onPreviewBlock={setBlockPreview}
|
|
503
|
-
onLint={handleLint}
|
|
504
|
-
linting={linting}
|
|
505
|
-
lintFindingCount={
|
|
506
|
-
lintFindingsByFile={findingsByFile}
|
|
507
|
-
lintHasError={hasLintError}
|
|
502
|
+
onLint={lint.handleLint}
|
|
503
|
+
linting={lint.linting}
|
|
504
|
+
lintFindingCount={lint.lintFindingCount}
|
|
505
|
+
lintFindingsByFile={lint.findingsByFile}
|
|
506
|
+
lintHasError={lint.hasLintError}
|
|
508
507
|
onAddAssetToTimeline={handleAddAssetAtPlayhead}
|
|
509
508
|
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
|
|
510
509
|
/>
|
|
@@ -577,8 +576,8 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
577
576
|
<StudioOverlays
|
|
578
577
|
projectId={projectId}
|
|
579
578
|
projectDir={fileManager.projectDir}
|
|
580
|
-
lintModal={lintModal}
|
|
581
|
-
closeLintModal={closeLintModal}
|
|
579
|
+
lintModal={lint.lintModal}
|
|
580
|
+
closeLintModal={lint.closeLintModal}
|
|
582
581
|
consoleErrors={consoleErrors}
|
|
583
582
|
clearConsoleErrors={() => setConsoleErrors(null)}
|
|
584
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
|
|
|
@@ -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
|
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { expect, it, vi } from "vitest";
|
|
6
|
+
import { useApplyColorGradingScope } from "./useApplyColorGradingScope";
|
|
7
|
+
|
|
8
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
|
|
10
|
+
const readProjectFile = vi.fn(async (_path: string) => "<html><body></body></html>");
|
|
11
|
+
|
|
12
|
+
vi.mock("../contexts/StudioContext", () => ({
|
|
13
|
+
useStudioShellContext: () => ({
|
|
14
|
+
projectId: "demo",
|
|
15
|
+
activeCompPath: "index.html",
|
|
16
|
+
showToast: vi.fn(),
|
|
17
|
+
waitForPendingDomEditSaves: async () => {},
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
vi.mock("../contexts/DomEditContext", () => ({
|
|
21
|
+
useDomEditContext: () => ({ domEditSelection: null }),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock("../contexts/FileManagerContext", () => ({
|
|
24
|
+
useFileManagerContext: () => ({
|
|
25
|
+
readProjectFile,
|
|
26
|
+
writeProjectFile: vi.fn(),
|
|
27
|
+
fileTree: ["index.html", "scene.html", ".hyperframes/preview/4884df6e.html"],
|
|
28
|
+
compositions: ["index.html", "scene.html"],
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
it("grades the project's compositions, not Studio's generated preview documents", async () => {
|
|
33
|
+
let apply: ReturnType<typeof useApplyColorGradingScope> | null = null;
|
|
34
|
+
function Probe() {
|
|
35
|
+
apply = useApplyColorGradingScope(
|
|
36
|
+
vi.fn(async () => {}),
|
|
37
|
+
vi.fn(),
|
|
38
|
+
);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const root = createRoot(document.createElement("div"));
|
|
42
|
+
act(() => root.render(<Probe />));
|
|
43
|
+
|
|
44
|
+
await act(async () => {
|
|
45
|
+
await apply!("project", null);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(readProjectFile.mock.calls.map(([path]) => path).sort()).toEqual([
|
|
49
|
+
"index.html",
|
|
50
|
+
"scene.html",
|
|
51
|
+
]);
|
|
52
|
+
act(() => root.unmount());
|
|
53
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
3
|
+
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
4
|
+
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
5
|
+
import {
|
|
6
|
+
applyColorGradingScopeUpdate,
|
|
7
|
+
EMPTY_COLOR_GRADING_SCOPE_RESULT,
|
|
8
|
+
type ColorGradingScope,
|
|
9
|
+
} from "../components/studioColorGradingScope";
|
|
10
|
+
import type { StudioRightPanelsProps } from "../components/StudioRightPanels.types";
|
|
11
|
+
|
|
12
|
+
export function useApplyColorGradingScope(
|
|
13
|
+
recordEdit: StudioRightPanelsProps["recordEdit"],
|
|
14
|
+
reloadPreview: StudioRightPanelsProps["reloadPreview"],
|
|
15
|
+
) {
|
|
16
|
+
const { projectId, activeCompPath, showToast, waitForPendingDomEditSaves } =
|
|
17
|
+
useStudioShellContext();
|
|
18
|
+
const { domEditSelection } = useDomEditContext();
|
|
19
|
+
const { readProjectFile, writeProjectFile, compositions } = useFileManagerContext();
|
|
20
|
+
return useCallback(
|
|
21
|
+
async (scope: ColorGradingScope, value: string | null) =>
|
|
22
|
+
applyColorGradingScopeUpdate({
|
|
23
|
+
scope,
|
|
24
|
+
value,
|
|
25
|
+
selectedSourceFile: domEditSelection?.sourceFile || activeCompPath || "index.html",
|
|
26
|
+
compositionPaths: compositions,
|
|
27
|
+
projectId,
|
|
28
|
+
waitForPendingDomEditSaves,
|
|
29
|
+
readProjectFile,
|
|
30
|
+
writeProjectFile,
|
|
31
|
+
recordEdit,
|
|
32
|
+
reloadPreview,
|
|
33
|
+
showToast,
|
|
34
|
+
}).catch((error) => {
|
|
35
|
+
showToast(
|
|
36
|
+
`Couldn't apply color grading: ${error instanceof Error ? error.message : String(error)}`,
|
|
37
|
+
"error",
|
|
38
|
+
);
|
|
39
|
+
return EMPTY_COLOR_GRADING_SCOPE_RESULT;
|
|
40
|
+
}),
|
|
41
|
+
[
|
|
42
|
+
activeCompPath,
|
|
43
|
+
compositions,
|
|
44
|
+
domEditSelection?.sourceFile,
|
|
45
|
+
projectId,
|
|
46
|
+
readProjectFile,
|
|
47
|
+
recordEdit,
|
|
48
|
+
reloadPreview,
|
|
49
|
+
showToast,
|
|
50
|
+
waitForPendingDomEditSaves,
|
|
51
|
+
writeProjectFile,
|
|
52
|
+
],
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -69,6 +69,21 @@ describe("useLintModal hasLintError", () => {
|
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
describe("useLintModal lintFindingCount", () => {
|
|
73
|
+
it("counts findings, not files, with the result open and after it closes", async () => {
|
|
74
|
+
const findings = ["a", "b", "c"].map((message) => ({
|
|
75
|
+
severity: "warning",
|
|
76
|
+
message,
|
|
77
|
+
file: "index.html",
|
|
78
|
+
}));
|
|
79
|
+
await mountWithLint(async () => ({ json: async () => ({ findings }) }));
|
|
80
|
+
expect(latest?.lintFindingCount).toBe(3);
|
|
81
|
+
act(() => latest?.closeLintModal());
|
|
82
|
+
expect(latest?.findingsByFile.size).toBe(1);
|
|
83
|
+
expect(latest?.lintFindingCount).toBe(3);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
72
87
|
describe("useLintModal background lint", () => {
|
|
73
88
|
it("waits for the live preview to boot before asking the server to lint", async () => {
|
|
74
89
|
usePlayerStore.setState({ previewBooted: false });
|
|
@@ -98,8 +98,10 @@ export function useLintModal(projectId: string | null, refreshKey?: number) {
|
|
|
98
98
|
|
|
99
99
|
const findingsByElement = useMemo(() => groupFindings((f) => f.elementId), [groupFindings]);
|
|
100
100
|
const findingsByFile = useMemo(() => groupFindings((f) => f.file), [groupFindings]);
|
|
101
|
-
//
|
|
102
|
-
const
|
|
101
|
+
// The badge counts findings, not files: the manual result when present, else the background one.
|
|
102
|
+
const badgeFindings = lintModal ?? backgroundFindings;
|
|
103
|
+
const lintFindingCount = badgeFindings.length;
|
|
104
|
+
const hasLintError = badgeFindings.some((f) => f.severity === "error");
|
|
103
105
|
|
|
104
106
|
// Sync lint findings directly to the player store — eliminates the
|
|
105
107
|
// mirroring useEffect that was previously in App.tsx.
|
|
@@ -115,6 +117,7 @@ export function useLintModal(projectId: string | null, refreshKey?: number) {
|
|
|
115
117
|
backgroundFindings,
|
|
116
118
|
findingsByElement,
|
|
117
119
|
findingsByFile,
|
|
120
|
+
lintFindingCount,
|
|
118
121
|
hasLintError,
|
|
119
122
|
};
|
|
120
123
|
}
|
|
@@ -51,7 +51,7 @@ async function readGroup(
|
|
|
51
51
|
* full parse, so large projects don't pay N openComposition calls.
|
|
52
52
|
*/
|
|
53
53
|
export function useProjectCompositionVariables(
|
|
54
|
-
|
|
54
|
+
compositionPaths: string[],
|
|
55
55
|
excludePath: string | null,
|
|
56
56
|
readProjectFile: (path: string) => Promise<string>,
|
|
57
57
|
refreshKey: unknown,
|
|
@@ -60,7 +60,7 @@ export function useProjectCompositionVariables(
|
|
|
60
60
|
|
|
61
61
|
useEffect(() => {
|
|
62
62
|
let cancelled = false;
|
|
63
|
-
const htmlFiles =
|
|
63
|
+
const htmlFiles = compositionPaths.filter((p) => p !== excludePath);
|
|
64
64
|
|
|
65
65
|
void (async () => {
|
|
66
66
|
const out: CompositionVariableGroup[] = [];
|
|
@@ -75,7 +75,7 @@ export function useProjectCompositionVariables(
|
|
|
75
75
|
cancelled = true;
|
|
76
76
|
};
|
|
77
77
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
-
}, [
|
|
78
|
+
}, [compositionPaths, excludePath, readProjectFile, refreshKey]);
|
|
79
79
|
|
|
80
80
|
return groups;
|
|
81
81
|
}
|