@hyperframes/studio 0.8.51 → 0.8.53-desktop.0
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-Cn_PpHqP.js → hyperframes-player-PTEnAHhg.js} +1 -1
- package/dist/assets/{index-CPS3Cvj8.js → index--myXp8ui.js} +1 -1
- package/dist/assets/{index-Clrb11lj.js → index-BjslOS9g.js} +1 -1
- package/dist/assets/index-BnXZuIJD.css +1 -0
- package/dist/assets/index-CNyviK_l.js +390 -0
- package/dist/index.css +29 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +808 -12
- package/dist/index.html +2 -2
- package/dist/index.js +8109 -7583
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/src/App.tsx +45 -50
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/components/EditorShell.selectionSync.test.tsx +9 -4
- package/src/components/EditorShell.tsx +56 -56
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.test.ts +0 -12
- package/src/components/StudioHeader.tsx +117 -160
- package/src/components/StudioLeftPanels.tsx +202 -0
- package/src/components/{StudioRightPanel.tsx → StudioRightPanels.tsx} +57 -194
- package/src/components/{StudioRightPanel.types.ts → StudioRightPanels.types.ts} +4 -3
- package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
- package/src/components/TimelineHistoryButtons.tsx +54 -0
- package/src/components/TimelineToolbar.test.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +3 -7
- package/src/components/dock/Dock.test.tsx +100 -18
- package/src/components/dock/Dock.tsx +32 -5
- package/src/components/dock/dock.css +5 -0
- package/src/components/dock/dockAccessibility.test.ts +166 -0
- package/src/components/dock/dockAccessibility.ts +98 -0
- package/src/components/dock/dockLayout.test.ts +134 -0
- package/src/components/dock/dockLayout.ts +53 -6
- package/src/components/dock/dockLayoutStore.test.ts +28 -0
- package/src/components/dock/dockLayoutStore.ts +21 -4
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +4 -2
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/SnapToolbar.test.tsx +0 -3
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +3 -1
- package/src/components/editor/useColorGradingController.test.ts +38 -0
- package/src/components/editor/useColorGradingController.ts +9 -1
- package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
- package/src/components/editor/useColorGradingPreviews.ts +11 -1
- package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
- package/src/components/editor/usePreviewCompositionRect.ts +21 -0
- package/src/components/nle/NLEContext.tsx +0 -43
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +1 -1
- package/src/components/nle/TimelinePane.tsx +53 -67
- package/src/components/panels/SlideshowPanel.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +1 -1
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/RenderQueue.tsx +1 -1
- package/src/components/renders/RenderQueuePanel.tsx +1 -1
- package/src/components/sidebar/{LeftSidebar.masterComposition.test.tsx → CompositionsPanel.masterComposition.test.tsx} +6 -9
- package/src/components/sidebar/CompositionsPanel.tsx +15 -0
- package/src/components/sidebar/{LeftSidebar.lintBadge.test.tsx → SidebarLintButton.test.tsx} +6 -11
- package/src/components/sidebar/SidebarLintButton.tsx +50 -0
- package/src/components/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +222 -30
- package/src/components/ui/Button.tsx +88 -125
- package/src/components/ui/IconButton.tsx +38 -0
- package/src/components/ui/Input.tsx +115 -0
- package/src/components/ui/Menu.test.tsx +374 -0
- package/src/components/ui/Menu.tsx +213 -0
- package/src/components/ui/NumberField.tsx +122 -0
- package/src/components/ui/Popover.tsx +74 -0
- package/src/components/ui/Select.tsx +100 -0
- package/src/components/ui/Slider.tsx +153 -0
- package/src/components/ui/Tabs.test.tsx +98 -0
- package/src/components/ui/Tabs.tsx +71 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/Tooltip.test.tsx +110 -0
- package/src/components/ui/Tooltip.tsx +38 -112
- package/src/components/ui/cn.test.ts +30 -0
- package/src/components/ui/cn.ts +25 -0
- package/src/components/ui/index.ts +29 -2
- package/src/components/ui/mountHost.testHelpers.ts +24 -0
- package/src/components/ui/valueControls.test.tsx +524 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -57
- package/src/hooks/appHotkeysDispatch.ts +3 -4
- package/src/hooks/timelineEditPermission.ts +31 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -2
- package/src/hooks/useAppHotkeys.test.ts +0 -1
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -3
- package/src/hooks/useAppHotkeys.ts +20 -83
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useDomEditPreviewSync.ts +4 -6
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useDomEditWiring.ts +4 -9
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useFileManager.ts +21 -123
- package/src/hooks/usePanelLayout.test.ts +108 -296
- package/src/hooks/usePanelLayout.ts +50 -255
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/useProjectFileWriter.ts +150 -0
- package/src/hooks/useRightPanelIntents.test.ts +181 -0
- package/src/hooks/useRightPanelIntents.ts +63 -0
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/hooks/useSlideshowTabState.test.ts +9 -31
- package/src/hooks/useSlideshowTabState.ts +6 -19
- package/src/hooks/useStudioContextValue.test.ts +6 -18
- package/src/hooks/useStudioContextValue.ts +5 -26
- package/src/hooks/useTimelineDeleteOps.test.tsx +111 -0
- package/src/hooks/useTimelineEditing.test.tsx +322 -3
- package/src/hooks/useTimelineEditing.ts +103 -16
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTrackPendingTimelineEdit.test.tsx +62 -0
- package/src/hooks/useTrackPendingTimelineEdit.ts +22 -0
- package/src/hostMount.test.tsx +222 -0
- package/src/index.ts +87 -0
- package/src/player/components/AutomationSelectionMenu.tsx +1 -1
- package/src/player/components/Timeline.tsx +1 -1
- package/src/player/components/TimelineCanvas.tsx +4 -4
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +30 -30
- package/src/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/lib/timelinePerformanceDiagnostics.test.ts +3 -2
- package/src/player/lib/timelinePerformanceDiagnostics.ts +1 -1
- package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
- package/src/player/store/previewIframeStore.test.tsx +29 -0
- package/src/player/store/previewIframeStore.ts +25 -0
- package/src/renderQueueExports.test.tsx +58 -0
- package/src/styles/classCandidates.test.ts +105 -0
- package/src/styles/classCandidates.ts +281 -0
- package/src/styles/hex-baseline.json +84 -0
- package/src/styles/hexRatchet.test.ts +201 -0
- package/src/styles/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/theme.css +57 -0
- package/src/styles/theme.test.ts +2 -15
- package/src/styles/tokenGate.test.ts +109 -0
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioHelpers.test.ts +18 -0
- package/src/utils/studioHelpers.ts +11 -8
- package/src/utils/studioUiPreferences.test.ts +10 -16
- package/src/utils/studioUiPreferences.ts +0 -16
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-C05SNrvY.js +0 -360
- package/dist/assets/index-DUR9qzQM.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
- package/src/components/StudioLeftSidebar.tsx +0 -207
- package/src/components/nle/TimelineResizeDivider.tsx +0 -101
- package/src/components/sidebar/LeftSidebar.storage.test.ts +0 -28
- package/src/components/sidebar/LeftSidebar.tsx +0 -348
- package/src/hooks/useInspectorSplitResize.ts +0 -51
- package/src/utils/fitPanels.test.ts +0 -164
- package/src/utils/fitPanels.ts +0 -151
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// The hex ratchet (R12, AE2): a per-file baseline of colour literals that only ever goes down.
|
|
2
|
+
// Per file so two sweep PRs touching one file conflict on its line and force a recount.
|
|
3
|
+
// The baseline is never regenerated by a normal run: a missing one fails, and rewriting it needs
|
|
4
|
+
// the named flag, so the number in git is one a human accepted.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { listSourceFiles, REPO_ROOT, STYLES_DIR } from "./styleSources";
|
|
10
|
+
|
|
11
|
+
const BASELINE_PATH = path.join(STYLES_DIR, "hex-baseline.json");
|
|
12
|
+
|
|
13
|
+
const WRITE_FLAG = "HEX_BASELINE_WRITE";
|
|
14
|
+
const LOWER_COMMAND = `${WRITE_FLAG}=1 bunx vitest run src/styles/hexRatchet.test.ts`;
|
|
15
|
+
|
|
16
|
+
// A literal colour: `#abc`, `#aabbcc`, `#aabbccdd`, or functional notation. This test owns the
|
|
17
|
+
// regex, so the baseline is whatever this rule sees.
|
|
18
|
+
const COLOR_LITERAL = /#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})\b|\b(?:rgba?|hsla?)\(/gi;
|
|
19
|
+
|
|
20
|
+
/** Files whose whole job is to hold colour values. */
|
|
21
|
+
function isTokenSource(relative: string): boolean {
|
|
22
|
+
return /styles\/(theme\.css|tailwind-preset\.shared\.js|tailwind-preset\.ts)$/.test(relative);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isScanned(relative: string): boolean {
|
|
26
|
+
if (isTokenSource(relative) || /\.test\.tsx?$/.test(relative)) return false;
|
|
27
|
+
return /\.(tsx?|css)$/.test(relative);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Strings match first and are kept, so `"image/*"` or `"a//b"` never opens a comment. */
|
|
31
|
+
const STRING_OR_COMMENT =
|
|
32
|
+
/("(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`)|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
|
|
33
|
+
|
|
34
|
+
/** Comments hold issue numbers like `#2291` that read as hex; count code only. */
|
|
35
|
+
function stripComments(text: string, lineComments: boolean): string {
|
|
36
|
+
return text.replace(STRING_OR_COMMENT, (match, string?: string) => {
|
|
37
|
+
if (string !== undefined) return match;
|
|
38
|
+
return match.startsWith("//") && !lineComments ? match : "";
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function countColorLiterals(text: string, lineComments = true): number {
|
|
43
|
+
return [...stripComments(text, lineComments).matchAll(COLOR_LITERAL)].length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Repository-relative path to colour-literal count, for every scanned file. */
|
|
47
|
+
function scan(): Map<string, number> {
|
|
48
|
+
const counts = new Map<string, number>();
|
|
49
|
+
for (const [file, text] of listSourceFiles(isScanned, REPO_ROOT)) {
|
|
50
|
+
const count = countColorLiterals(text, !file.endsWith(".css"));
|
|
51
|
+
if (count > 0) counts.set(file, count);
|
|
52
|
+
}
|
|
53
|
+
return counts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface Baseline {
|
|
57
|
+
readonly total: number;
|
|
58
|
+
readonly files: Readonly<Record<string, number>>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toBaseline(counts: ReadonlyMap<string, number>): Baseline {
|
|
62
|
+
const files: Record<string, number> = {};
|
|
63
|
+
let total = 0;
|
|
64
|
+
for (const key of [...counts.keys()].sort()) {
|
|
65
|
+
files[key] = counts.get(key) ?? 0;
|
|
66
|
+
total += files[key];
|
|
67
|
+
}
|
|
68
|
+
return { total, files };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every file whose count moved, split by direction. */
|
|
72
|
+
function compare(counts: ReadonlyMap<string, number>, baseline: Baseline) {
|
|
73
|
+
const risen: string[] = [];
|
|
74
|
+
const fallen: string[] = [];
|
|
75
|
+
// A file missing from the baseline has a baseline of zero, so a rename
|
|
76
|
+
// cannot smuggle its colours past the ratchet.
|
|
77
|
+
for (const [file, count] of counts) {
|
|
78
|
+
const allowed = baseline.files[file] ?? 0;
|
|
79
|
+
if (count > allowed) risen.push(`${file}: ${count} colour literals, baseline ${allowed}`);
|
|
80
|
+
if (count < allowed) fallen.push(`${file}: ${count}, baseline ${allowed}`);
|
|
81
|
+
}
|
|
82
|
+
for (const [file, allowed] of Object.entries(baseline.files)) {
|
|
83
|
+
if (!counts.has(file)) fallen.push(`${file}: now 0, baseline ${allowed}`);
|
|
84
|
+
}
|
|
85
|
+
return { risen, fallen };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A rise is a failure. A fall is reported with the command that banks it, because a baseline
|
|
89
|
+
// left high is a budget the next change can spend.
|
|
90
|
+
function verdict(counts: ReadonlyMap<string, number>, baseline: Baseline | undefined): string[] {
|
|
91
|
+
if (baseline === undefined) {
|
|
92
|
+
return [`src/styles/hex-baseline.json is missing. Write it with: ${LOWER_COMMAND}`];
|
|
93
|
+
}
|
|
94
|
+
const { risen, fallen } = compare(counts, baseline);
|
|
95
|
+
if (risen.length > 0) {
|
|
96
|
+
return [
|
|
97
|
+
"Colour literals belong in src/styles/theme.css.",
|
|
98
|
+
...risen,
|
|
99
|
+
`If these are unavoidable, lower is the only direction: ${LOWER_COMMAND}`,
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
if (fallen.length > 0) return [`Colour literals went down. Bank it: ${LOWER_COMMAND}`, ...fallen];
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readBaseline(): Baseline | undefined {
|
|
107
|
+
if (!existsSync(BASELINE_PATH)) return undefined;
|
|
108
|
+
return JSON.parse(readFileSync(BASELINE_PATH, "utf8")) as Baseline;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
describe("colour literal counter", () => {
|
|
112
|
+
it("counts a hex in a style object, in canvas code and in a stylesheet", () => {
|
|
113
|
+
expect(countColorLiterals(`<div style={{ background: "#161618" }} />`)).toBe(1);
|
|
114
|
+
expect(countColorLiterals(`context.fillStyle = "#090b0e";`)).toBe(1);
|
|
115
|
+
expect(countColorLiterals(`.ring { box-shadow: 0 0 0 1px rgb(110 231 183); }`)).toBe(1);
|
|
116
|
+
expect(countColorLiterals(`a { color: rgba(0, 0, 0, 0.5); border-color: hsl(0 0% 4%); }`)).toBe(
|
|
117
|
+
2,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does not mistake a fragment or an issue number for a colour", () => {
|
|
122
|
+
// Known limit: an identifier that is exactly 3, 4, 6 or 8 hex characters
|
|
123
|
+
// in code reads as a colour; the baseline absorbs it.
|
|
124
|
+
expect(countColorLiterals(`href="#section-two" // see #12345`)).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("keeps a colour that follows comment-shaped text inside a string", () => {
|
|
128
|
+
expect(countColorLiterals(`accept="image/*"; const c = "#161618"; x = "*/";`)).toBe(1);
|
|
129
|
+
expect(countColorLiterals("const u = `x//y`; const c = '#161618';")).toBe(1);
|
|
130
|
+
expect(countColorLiterals(`.a { background: url(//cdn.test/a.png) #161618; }`, false)).toBe(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("ignores a colour-shaped issue number in a line or block comment", () => {
|
|
134
|
+
expect(countColorLiterals(`const a = 1; // decision (#2291)`)).toBe(0);
|
|
135
|
+
expect(countColorLiterals(`/** see #2291 and rgba(0,0,0,1) */\nconst a = 1;`)).toBe(0);
|
|
136
|
+
expect(countColorLiterals(`fetch("https://x.test/#abc"); const c = "#161618"; // #2291`)).toBe(
|
|
137
|
+
2,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("leaves the token sources and the tests out of the scan", () => {
|
|
142
|
+
expect(isScanned("packages/studio/src/styles/theme.css")).toBe(false);
|
|
143
|
+
expect(isScanned("packages/studio/src/styles/tailwind-preset.shared.js")).toBe(false);
|
|
144
|
+
expect(isScanned("packages/studio/src/styles/theme.test.ts")).toBe(false);
|
|
145
|
+
expect(isScanned("packages/studio/src/components/ui/Button.tsx")).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("hex ratchet", () => {
|
|
150
|
+
const baseline: Baseline = { total: 2, files: { "a.tsx": 2 } };
|
|
151
|
+
|
|
152
|
+
it("fails when a file's count rises, naming the file and both numbers", () => {
|
|
153
|
+
// Covers AE2.
|
|
154
|
+
const message = verdict(new Map([["a.tsx", 3]]), baseline).join("\n");
|
|
155
|
+
|
|
156
|
+
expect(message).toContain("a.tsx: 3 colour literals, baseline 2");
|
|
157
|
+
expect(message).toContain(LOWER_COMMAND);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("passes when a count falls and says how to bank it", () => {
|
|
161
|
+
const message = verdict(new Map([["a.tsx", 1]]), baseline).join("\n");
|
|
162
|
+
|
|
163
|
+
expect(message).toContain("Colour literals went down");
|
|
164
|
+
expect(message).toContain(LOWER_COMMAND);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("treats a file the baseline has never seen as a baseline of zero", () => {
|
|
168
|
+
const empty: Baseline = { total: 0, files: {} };
|
|
169
|
+
|
|
170
|
+
expect(verdict(new Map([["new.tsx", 0]]), empty)).toEqual([]);
|
|
171
|
+
expect(verdict(new Map([["new.tsx", 1]]), empty).join("\n")).toContain(
|
|
172
|
+
"new.tsx: 1 colour literals, baseline 0",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("round-trips: a baseline written from a scan accepts that same scan", () => {
|
|
177
|
+
const counts = new Map([
|
|
178
|
+
["b.css", 4],
|
|
179
|
+
["a.tsx", 2],
|
|
180
|
+
]);
|
|
181
|
+
const written = toBaseline(counts);
|
|
182
|
+
|
|
183
|
+
expect(Object.keys(written.files)).toEqual(["a.tsx", "b.css"]);
|
|
184
|
+
expect(written.total).toBe(6);
|
|
185
|
+
expect(verdict(counts, written)).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("fails with the flag named when there is no baseline at all", () => {
|
|
189
|
+
expect(verdict(new Map([["a.tsx", 0]]), undefined).join("\n")).toContain(`${WRITE_FLAG}=1`);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("holds Studio at or below its committed colour-literal baseline", () => {
|
|
193
|
+
const counts = scan();
|
|
194
|
+
if (process.env[WRITE_FLAG] === "1") {
|
|
195
|
+
writeFileSync(BASELINE_PATH, `${JSON.stringify(toBaseline(counts), null, 2)}\n`);
|
|
196
|
+
}
|
|
197
|
+
const committed = readBaseline();
|
|
198
|
+
|
|
199
|
+
expect(verdict(counts, committed)).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { listSourceFiles, toPosixPath } from "./styleSources";
|
|
3
|
+
|
|
4
|
+
describe("style source paths", () => {
|
|
5
|
+
it("turns a Windows-style relative path into the forward-slash form the baseline uses", () => {
|
|
6
|
+
expect(toPosixPath("packages\\studio\\src\\App.tsx")).toBe("packages/studio/src/App.tsx");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("leaves a forward-slash path alone", () => {
|
|
10
|
+
expect(toPosixPath("packages/studio/src/App.tsx")).toBe("packages/studio/src/App.tsx");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("hands the keep filter and the map key the same forward-slash path", () => {
|
|
14
|
+
const seen: string[] = [];
|
|
15
|
+
const files = listSourceFiles((file) => (seen.push(file), file.endsWith("styleSources.ts")));
|
|
16
|
+
|
|
17
|
+
expect([...files.keys()]).toEqual(["styles/styleSources.ts"]);
|
|
18
|
+
expect(seen.every((file) => !file.includes("\\"))).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Where the style tests read from: Tailwind's entry resolved from `node_modules`, and the file list
|
|
2
|
+
// that `@source` in `studio.css` describes, shared so the gate, ratchet and theme test agree.
|
|
3
|
+
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
export const STYLES_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const SRC_DIR = path.dirname(STYLES_DIR);
|
|
11
|
+
export const REPO_ROOT = path.resolve(SRC_DIR, "../../..");
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
export const TAILWIND_DIR = path.dirname(require.resolve("tailwindcss/package.json"));
|
|
15
|
+
|
|
16
|
+
/** Resolves `@import` for Tailwind's compiler: bare `tailwindcss`, or a path. */
|
|
17
|
+
export function loadStylesheet(id: string, base: string) {
|
|
18
|
+
const file = id === "tailwindcss" ? path.join(TAILWIND_DIR, "index.css") : path.resolve(base, id);
|
|
19
|
+
return { path: file, base: path.dirname(file), content: readFileSync(file, "utf8") };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Forward slashes on every platform, so keys match the baseline file and the gate's messages. */
|
|
23
|
+
export const toPosixPath = (relativePath: string): string => relativePath.replace(/\\/g, "/");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Studio's own sources, keyed by path relative to `from`, for every file the
|
|
27
|
+
* caller keeps. Build output and dependencies are never walked.
|
|
28
|
+
*/
|
|
29
|
+
export function listSourceFiles(
|
|
30
|
+
keep: (relativePath: string) => boolean,
|
|
31
|
+
from: string = SRC_DIR,
|
|
32
|
+
): Map<string, string> {
|
|
33
|
+
const files = new Map<string, string>();
|
|
34
|
+
const walk = (dir: string) => {
|
|
35
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
const full = path.join(dir, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
if (entry.name !== "node_modules" && entry.name !== "dist") walk(full);
|
|
39
|
+
} else {
|
|
40
|
+
const relative = toPosixPath(path.relative(from, full));
|
|
41
|
+
if (keep(relative)) files.set(relative, readFileSync(full, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
walk(SRC_DIR);
|
|
46
|
+
return files;
|
|
47
|
+
}
|
package/src/styles/theme.css
CHANGED
|
@@ -41,12 +41,48 @@
|
|
|
41
41
|
--color-media: #00e3ff;
|
|
42
42
|
--color-container: #f5a623;
|
|
43
43
|
|
|
44
|
+
/* The two stops of the HeyGen brand gradient, worn by the header logo. Fixed brand values
|
|
45
|
+
* rather than roles, so they are not reachable as a background or text colour by mistake. */
|
|
46
|
+
--color-brand-gradient-from: #06e3fa;
|
|
47
|
+
--color-brand-gradient-to: #4fdb5e;
|
|
48
|
+
|
|
49
|
+
/* The timeline's own surfaces: darker and bluer than the shared scale, so they
|
|
50
|
+
* get their own names instead of snapping to the nearest --color-bg/-surface
|
|
51
|
+
* step (AD147: a host themes the timeline by overriding these). */
|
|
52
|
+
--timeline-shell-bg: #0a0a0b;
|
|
53
|
+
--timeline-shell-border: rgba(255, 255, 255, 0.05);
|
|
54
|
+
--timeline-ruler-border: rgba(255, 255, 255, 0.16);
|
|
55
|
+
--timeline-row-bg: #101014;
|
|
56
|
+
--timeline-row-border: rgba(255, 255, 255, 0.06);
|
|
57
|
+
--timeline-gutter-bg: #0e0f12;
|
|
58
|
+
--timeline-gutter-border: rgba(255, 255, 255, 0.1);
|
|
59
|
+
--timeline-text-primary: rgba(255, 255, 255, 0.92);
|
|
60
|
+
--timeline-text-secondary: rgba(255, 255, 255, 0.62);
|
|
61
|
+
--timeline-tick-text: rgba(255, 255, 255, 0.34);
|
|
62
|
+
--timeline-tick-major: rgba(255, 255, 255, 0.1);
|
|
63
|
+
--timeline-tick-minor: rgba(255, 255, 255, 0.06);
|
|
64
|
+
--timeline-clip-bg: #141922;
|
|
65
|
+
--timeline-clip-bg-active: #181e28;
|
|
66
|
+
--timeline-clip-border: rgba(255, 255, 255, 0.1);
|
|
67
|
+
--timeline-clip-border-hover: rgba(255, 255, 255, 0.18);
|
|
68
|
+
--timeline-clip-border-active: rgba(255, 255, 255, 0.24);
|
|
69
|
+
--timeline-handle: rgba(255, 255, 255, 0.2);
|
|
70
|
+
--timeline-resize-seam: rgba(255, 255, 255, 0.12);
|
|
71
|
+
--timeline-resize-active: rgba(255, 255, 255, 0.24);
|
|
72
|
+
--timeline-track-clip-fill: rgba(255, 255, 255, 0.055);
|
|
73
|
+
--timeline-track-clip-active: rgba(60, 230, 172, 0.16);
|
|
74
|
+
--timeline-track-label: rgba(255, 255, 255, 0.5);
|
|
75
|
+
|
|
44
76
|
/* -------------------------------------------------------------- geometry */
|
|
45
77
|
|
|
46
78
|
--radius-sm: 0.25rem;
|
|
47
79
|
--radius-md: 0.375rem;
|
|
48
80
|
--radius-lg: 0.5rem;
|
|
49
81
|
|
|
82
|
+
/* The one radius every push-button wears, named by its role so a button does
|
|
83
|
+
* not have to know which step of the scale it sits on. */
|
|
84
|
+
--radius-button: var(--radius-md);
|
|
85
|
+
|
|
50
86
|
/* Control heights: 24, 28 and 32 px. `--spacing-*` gives `h-ctl`, `min-h-ctl`
|
|
51
87
|
* and friends. */
|
|
52
88
|
--spacing-ctl-sm: 24px;
|
|
@@ -80,12 +116,23 @@
|
|
|
80
116
|
--shadow-menu: 0 8px 24px rgb(0 0 0 / 0.45), 0 0 0 1px rgb(0 0 0 / 0.4);
|
|
81
117
|
--shadow-popover: 0 16px 40px rgb(0 0 0 / 0.55), 0 0 0 1px rgb(0 0 0 / 0.45);
|
|
82
118
|
|
|
119
|
+
/* The primary button is the one light surface in a dark UI, so it reads as a
|
|
120
|
+
* raised key rather than a floating panel: a tight contact shadow, a soft
|
|
121
|
+
* ambient one, and an inner top highlight. */
|
|
122
|
+
--shadow-btn-primary:
|
|
123
|
+
0 1px 2px rgb(0 0 0 / 0.6), 0 2px 6px rgb(0 0 0 / 0.35), inset 0 1px 0 rgb(255 255 255 / 0.65);
|
|
124
|
+
|
|
83
125
|
/* ---------------------------------------------------------------- motion */
|
|
84
126
|
|
|
85
127
|
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
|
|
86
128
|
--ease-out-expo: cubic-bezier(0.23, 1, 0.32, 1);
|
|
87
129
|
--ease-in-out-circ: cubic-bezier(0.77, 0, 0.175, 1);
|
|
88
130
|
|
|
131
|
+
/* The curve for a small state change (hover, press, focus), named by role so
|
|
132
|
+
* a control does not have to pick one. The other two stay for motion that is
|
|
133
|
+
* meant to be noticed. */
|
|
134
|
+
--ease-standard: var(--ease-out-quint);
|
|
135
|
+
|
|
89
136
|
/* Durations have no Tailwind namespace, so these are plain variables paired
|
|
90
137
|
* with the `@utility` rules at the bottom of this file. All are at or under
|
|
91
138
|
* 200 ms. */
|
|
@@ -126,11 +173,21 @@
|
|
|
126
173
|
--color-panel-text-3: var(--color-text-3);
|
|
127
174
|
--color-panel-text-4: var(--color-text-4);
|
|
128
175
|
--color-panel-text-5: var(--color-text-5);
|
|
176
|
+
--color-panel-bg-soft: var(--color-bg-2);
|
|
177
|
+
--color-panel-bg-2: var(--color-bg-2);
|
|
129
178
|
--color-panel-accent: var(--color-accent);
|
|
130
179
|
--color-panel-danger: var(--color-danger);
|
|
131
180
|
--color-panel-media: var(--color-media);
|
|
132
181
|
--color-panel-container: var(--color-container);
|
|
133
182
|
|
|
183
|
+
/* Names Studio's markup invented that no config ever defined, so the classes
|
|
184
|
+
* compiled to nothing. Aliased to the token that was meant, rather than
|
|
185
|
+
* given values of their own, so there is still one source per decision.
|
|
186
|
+
* U12 rewrites the call sites and deletes these. */
|
|
187
|
+
--color-surface-hover: var(--color-hover);
|
|
188
|
+
--color-accent-red: var(--color-danger);
|
|
189
|
+
--text-2xs: var(--text-step-10);
|
|
190
|
+
|
|
134
191
|
/* ------------------------------------------- deprecated: Tailwind palette */
|
|
135
192
|
|
|
136
193
|
/* Verbatim Tailwind 4.3.3 defaults for stock colours markup still names, so
|
package/src/styles/theme.test.ts
CHANGED
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
* property, a utility that stops compiling, the stock Tailwind palette
|
|
5
5
|
* leaking back in, or the JS preset / legacy colours drifting from it. */
|
|
6
6
|
|
|
7
|
-
import { createRequire } from "node:module";
|
|
8
7
|
import { readFileSync } from "node:fs";
|
|
9
8
|
import path from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
9
|
import React, { act } from "react";
|
|
12
10
|
import { createRoot } from "react-dom/client";
|
|
13
11
|
import { Gear, IconContext } from "@phosphor-icons/react";
|
|
@@ -15,23 +13,12 @@ import { compile } from "tailwindcss";
|
|
|
15
13
|
import { afterEach, describe, expect, it } from "vitest";
|
|
16
14
|
import studioPreset from "./tailwind-preset.shared.js";
|
|
17
15
|
import { readIconTokens } from "./iconTokens";
|
|
16
|
+
import { loadStylesheet, STYLES_DIR, TAILWIND_DIR } from "./styleSources";
|
|
18
17
|
|
|
19
18
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
20
19
|
|
|
21
|
-
const STYLES_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const require = createRequire(import.meta.url);
|
|
23
|
-
const TAILWIND_DIR = path.dirname(require.resolve("tailwindcss/package.json"));
|
|
24
|
-
|
|
25
|
-
function loadStylesheet(id: string, base: string) {
|
|
26
|
-
const file = id === "tailwindcss" ? path.join(TAILWIND_DIR, "index.css") : path.resolve(base, id);
|
|
27
|
-
return { path: file, base: path.dirname(file), content: readFileSync(file, "utf8") };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
20
|
async function buildSource(source: string, candidates: string[]): Promise<string> {
|
|
31
|
-
const compiled = await compile(source, {
|
|
32
|
-
base: STYLES_DIR,
|
|
33
|
-
loadStylesheet,
|
|
34
|
-
});
|
|
21
|
+
const compiled = await compile(source, { base: STYLES_DIR, loadStylesheet });
|
|
35
22
|
return compiled.build(candidates);
|
|
36
23
|
}
|
|
37
24
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The token gate (R11, AE1). Tailwind silently drops a class it cannot compile, so `rounded-button`
|
|
2
|
+
// renders as no radius and nothing goes red. This compiles Studio's real entry stylesheet with every
|
|
3
|
+
// class the source claims and reports any candidate producing no selector as `file: class`.
|
|
4
|
+
// Tailwind is the only judge, so there is no allowlist; a failing class is fixed, never excused.
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { compile } from "tailwindcss";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { extractClassCandidates } from "./classCandidates";
|
|
11
|
+
import { listSourceFiles, loadStylesheet, STYLES_DIR } from "./styleSources";
|
|
12
|
+
|
|
13
|
+
// A class selector in the emitted CSS, escapes removed: `.hover\:bg-surface\/50` reads back as its candidate.
|
|
14
|
+
const CLASS_SELECTOR = /\.((?:\\.|[^\s.,{}()>+~:[\]#'"*/\\])+)/g;
|
|
15
|
+
|
|
16
|
+
// Tailwind's `group`/`peer` variant markers (`group/card`) produce no rule of their own, so no
|
|
17
|
+
// compiled selector can vouch for them. Nothing else is exempt.
|
|
18
|
+
const MARKERS = new Set(["group", "peer"]);
|
|
19
|
+
|
|
20
|
+
// `hf-` is HyperFrames' reserved prefix for semantic hooks, not utilities, so Tailwind is the
|
|
21
|
+
// wrong judge of them.
|
|
22
|
+
const HOOK_PREFIX = /^hf-/;
|
|
23
|
+
|
|
24
|
+
/** Compile Studio's stylesheet and return every class it can produce. */
|
|
25
|
+
async function resolvable(candidates: string[]): Promise<Set<string>> {
|
|
26
|
+
const compiled = await compile(readFileSync(path.join(STYLES_DIR, "studio.css"), "utf8"), {
|
|
27
|
+
base: STYLES_DIR,
|
|
28
|
+
loadStylesheet,
|
|
29
|
+
});
|
|
30
|
+
const css = compiled.build(candidates);
|
|
31
|
+
const selectors = new Set<string>();
|
|
32
|
+
for (const [, selector] of css.matchAll(CLASS_SELECTOR)) {
|
|
33
|
+
selectors.add(selector.replace(/\\(.)/g, "$1"));
|
|
34
|
+
}
|
|
35
|
+
return selectors;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// `file: class` for every class the sources claim that Tailwind cannot make. Sources are passed
|
|
39
|
+
// in so the fixtures exercise the same code path as the tree.
|
|
40
|
+
async function unresolved(sources: ReadonlyMap<string, string>): Promise<string[]> {
|
|
41
|
+
const claims = new Map<string, string[]>();
|
|
42
|
+
for (const [file, source] of sources) {
|
|
43
|
+
for (const candidate of extractClassCandidates(source)) {
|
|
44
|
+
const seen = claims.get(candidate.base);
|
|
45
|
+
if (seen) seen.push(file);
|
|
46
|
+
else claims.set(candidate.base, [file]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const produced = await resolvable([...claims.keys()]);
|
|
50
|
+
const failures: string[] = [];
|
|
51
|
+
for (const [candidate, files] of claims) {
|
|
52
|
+
if (produced.has(candidate) || MARKERS.has(candidate.split("/")[0])) continue;
|
|
53
|
+
if (HOOK_PREFIX.test(candidate)) continue;
|
|
54
|
+
for (const file of files) failures.push(`${file}: ${candidate}`);
|
|
55
|
+
}
|
|
56
|
+
return failures.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Every file the gate reads, mirroring the `@source` globs in `studio.css`. Tests are excluded:
|
|
60
|
+
// a fixture in a test is not markup.
|
|
61
|
+
function studioSources(): Map<string, string> {
|
|
62
|
+
return listSourceFiles((file) => /\.tsx?$/.test(file) && !/\.test\.tsx?$/.test(file));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("token gate", () => {
|
|
66
|
+
it("names the file and the class when a class resolves to nothing", async () => {
|
|
67
|
+
// AE1: `rounded-hologram` looks like a token name but is defined nowhere. The map is named
|
|
68
|
+
// `buttonSizes`, not `sizeStyles`: the `cn()` that consumes it puts it in front of the gate.
|
|
69
|
+
const failures = await unresolved(
|
|
70
|
+
new Map([
|
|
71
|
+
[
|
|
72
|
+
"ui/Button.tsx",
|
|
73
|
+
`const buttonSizes = { md: "h-8 rounded-hologram text-mega" };
|
|
74
|
+
const Button = ({ size }) => <button className={cn(buttonSizes[size])} />;`,
|
|
75
|
+
],
|
|
76
|
+
]),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(failures).toEqual(["ui/Button.tsx: rounded-hologram", "ui/Button.tsx: text-mega"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("accepts an arbitrary value and reports it as one", async () => {
|
|
83
|
+
const source = `<div className="text-[11px]" />`;
|
|
84
|
+
|
|
85
|
+
expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
|
|
86
|
+
expect(extractClassCandidates(source).filter((c) => c.arbitrary)).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("accepts variants, static utilities and Studio's own CSS classes", async () => {
|
|
90
|
+
// `timeline-clip` is a plain rule in `studio.css`, not a utility.
|
|
91
|
+
const source = `<div className="hover:bg-surface/50 text-center border-dashed timeline-clip" />`;
|
|
92
|
+
|
|
93
|
+
expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("leaves Studio's own hook prefix and Tailwind's variant markers alone", async () => {
|
|
97
|
+
const source = `<div className="group group/card peer hf-fx-node" />`;
|
|
98
|
+
|
|
99
|
+
expect(await unresolved(new Map([["a.tsx", source]]))).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("claims nothing for a template chunk that touches an interpolation", async () => {
|
|
103
|
+
expect(await unresolved(new Map([["a.tsx", "<div className={`w-${i}`} />"]]))).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("resolves every class Studio's own source claims", async () => {
|
|
107
|
+
expect(await unresolved(studioSources())).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
5
|
+
historyTooltipLabel,
|
|
5
6
|
findMatchingTimelineElementId,
|
|
6
7
|
findTimelineIdByAncestor,
|
|
7
8
|
resolveDroppedAssetDimensions,
|
|
@@ -217,3 +218,20 @@ describe("resolveElementTrack", () => {
|
|
|
217
218
|
expect(resolveElementTrack({ authoredTrack: undefined, track: 3.6 })).toBe(4);
|
|
218
219
|
});
|
|
219
220
|
});
|
|
221
|
+
|
|
222
|
+
describe("historyTooltipLabel", () => {
|
|
223
|
+
// A disabled control gets no pointer events, so its tooltip cannot be opened
|
|
224
|
+
// to read. The wording is checked here instead of through the DOM.
|
|
225
|
+
it("names the action that would be undone", () => {
|
|
226
|
+
expect(historyTooltipLabel("undo", "Move layer")).toMatch(/^Undo Move layer \(.+\)$/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("names the action that would be redone", () => {
|
|
230
|
+
expect(historyTooltipLabel("redo", "Move layer")).toMatch(/^Redo Move layer \(.+\)$/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("keeps the shortcut when the history is empty", () => {
|
|
234
|
+
expect(historyTooltipLabel("undo", undefined)).toMatch(/^Undo \(.+\)$/);
|
|
235
|
+
expect(historyTooltipLabel("redo", null)).toMatch(/^Redo \(.+\)$/);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -22,13 +22,6 @@ export type RightPanelTab =
|
|
|
22
22
|
| "block-params"
|
|
23
23
|
| "slideshow"
|
|
24
24
|
| "variables";
|
|
25
|
-
export type RightInspectorPane = "layers" | "design";
|
|
26
|
-
|
|
27
|
-
export interface RightInspectorPanes {
|
|
28
|
-
layers: boolean;
|
|
29
|
-
design: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
export interface AgentModalAnchorPoint {
|
|
33
26
|
x: number;
|
|
34
27
|
y: number;
|
|
@@ -119,13 +112,23 @@ export function shouldIgnoreHistoryShortcut(target: EventTarget | null): boolean
|
|
|
119
112
|
return isTypingTarget(target);
|
|
120
113
|
}
|
|
121
114
|
|
|
122
|
-
|
|
115
|
+
function getHistoryShortcutLabel(action: "undo" | "redo"): string {
|
|
123
116
|
const isMac =
|
|
124
117
|
typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
|
125
118
|
const modifier = isMac ? "Cmd" : "Ctrl";
|
|
126
119
|
return action === "undo" ? `${modifier}+Z` : `${modifier}+Shift+Z`;
|
|
127
120
|
}
|
|
128
121
|
|
|
122
|
+
/** The Undo / Redo tooltip: the shortcut always, the last action's name when there is one. */
|
|
123
|
+
export function historyTooltipLabel(
|
|
124
|
+
action: "undo" | "redo",
|
|
125
|
+
lastAction: string | null | undefined,
|
|
126
|
+
): string {
|
|
127
|
+
const shortcut = getHistoryShortcutLabel(action);
|
|
128
|
+
const verb = action === "undo" ? "Undo" : "Redo";
|
|
129
|
+
return lastAction ? `${verb} ${lastAction} (${shortcut})` : `${verb} (${shortcut})`;
|
|
130
|
+
}
|
|
131
|
+
|
|
129
132
|
export type ElementMatchSelection = Pick<
|
|
130
133
|
DomEditSelection,
|
|
131
134
|
| "id"
|
|
@@ -20,7 +20,6 @@ describe("studio UI preferences", () => {
|
|
|
20
20
|
const storage = createStorage();
|
|
21
21
|
|
|
22
22
|
writeStudioUiPreferences({ timelineVisible: false }, storage);
|
|
23
|
-
writeStudioUiPreferences({ leftWidth: 384, rightWidth: 424 }, storage);
|
|
24
23
|
writeStudioUiPreferences({ playbackRate: 1.5 }, storage);
|
|
25
24
|
writeStudioUiPreferences({ audioMuted: true }, storage);
|
|
26
25
|
writeStudioUiPreferences({ audioVolume: 0.4 }, storage);
|
|
@@ -28,8 +27,6 @@ describe("studio UI preferences", () => {
|
|
|
28
27
|
|
|
29
28
|
expect(readStudioUiPreferences(storage)).toEqual({
|
|
30
29
|
timelineVisible: false,
|
|
31
|
-
leftWidth: 384,
|
|
32
|
-
rightWidth: 424,
|
|
33
30
|
playbackRate: 1.5,
|
|
34
31
|
audioMuted: true,
|
|
35
32
|
audioVolume: 0.4,
|
|
@@ -57,9 +54,6 @@ describe("studio UI preferences", () => {
|
|
|
57
54
|
storage.setItem(
|
|
58
55
|
"hf-studio-ui-preferences",
|
|
59
56
|
JSON.stringify({
|
|
60
|
-
leftCollapsed: "yes",
|
|
61
|
-
leftWidth: "wide",
|
|
62
|
-
rightWidth: Number.NaN,
|
|
63
57
|
timelineVisible: true,
|
|
64
58
|
playbackRate: Number.NaN,
|
|
65
59
|
audioMuted: "false",
|
|
@@ -119,28 +113,28 @@ describe("thumbnailMode preference", () => {
|
|
|
119
113
|
describe("per-project scoping", () => {
|
|
120
114
|
it("keeps two projects' preferences independent", () => {
|
|
121
115
|
const storage = createStorage();
|
|
122
|
-
writeStudioUiPreferences({
|
|
123
|
-
writeStudioUiPreferences({
|
|
116
|
+
writeStudioUiPreferences({ playbackRate: 1.25 }, storage, "alpha");
|
|
117
|
+
writeStudioUiPreferences({ playbackRate: 2 }, storage, "beta");
|
|
124
118
|
|
|
125
|
-
expect(readStudioUiPreferences(storage, "alpha").
|
|
126
|
-
expect(readStudioUiPreferences(storage, "beta").
|
|
119
|
+
expect(readStudioUiPreferences(storage, "alpha").playbackRate).toBe(1.25);
|
|
120
|
+
expect(readStudioUiPreferences(storage, "beta").playbackRate).toBe(2);
|
|
127
121
|
});
|
|
128
122
|
|
|
129
123
|
it("a project with nothing saved yet inherits the pre-scoping global entry once", () => {
|
|
130
124
|
const storage = createStorage();
|
|
131
|
-
storage.setItem("hf-studio-ui-preferences", JSON.stringify({
|
|
125
|
+
storage.setItem("hf-studio-ui-preferences", JSON.stringify({ playbackRate: 0.5 }));
|
|
132
126
|
|
|
133
|
-
expect(readStudioUiPreferences(storage, "gamma").
|
|
127
|
+
expect(readStudioUiPreferences(storage, "gamma").playbackRate).toBe(0.5);
|
|
134
128
|
});
|
|
135
129
|
|
|
136
130
|
it("stops inheriting the global entry once the project has its own write", () => {
|
|
137
131
|
const storage = createStorage();
|
|
138
|
-
storage.setItem("hf-studio-ui-preferences", JSON.stringify({
|
|
132
|
+
storage.setItem("hf-studio-ui-preferences", JSON.stringify({ playbackRate: 0.5 }));
|
|
139
133
|
|
|
140
|
-
writeStudioUiPreferences({
|
|
134
|
+
writeStudioUiPreferences({ playbackRate: 1.75 }, storage, "gamma");
|
|
141
135
|
|
|
142
|
-
expect(readStudioUiPreferences(storage, "gamma").
|
|
143
|
-
expect(readStudioUiPreferences(storage, null).
|
|
136
|
+
expect(readStudioUiPreferences(storage, "gamma").playbackRate).toBe(1.75);
|
|
137
|
+
expect(readStudioUiPreferences(storage, null).playbackRate).toBe(0.5);
|
|
144
138
|
});
|
|
145
139
|
});
|
|
146
140
|
|