@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,29 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { expect, it } from "vitest";
|
|
5
|
+
import { useOwnPreviewIframe, usePreviewIframeStore } from "./previewIframeStore";
|
|
6
|
+
|
|
7
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
8
|
+
|
|
9
|
+
it("clears the store on owner unmount so a remount starts from null", () => {
|
|
10
|
+
const seen: Array<HTMLIFrameElement | null> = [];
|
|
11
|
+
const Owner = () => {
|
|
12
|
+
seen.push(useOwnPreviewIframe());
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
15
|
+
const iframe = document.createElement("iframe");
|
|
16
|
+
const first = createRoot(document.createElement("div"));
|
|
17
|
+
act(() => first.render(<Owner />));
|
|
18
|
+
act(() => usePreviewIframeStore.getState().setIframe(iframe));
|
|
19
|
+
expect(seen.at(-1)).toBe(iframe);
|
|
20
|
+
|
|
21
|
+
act(() => first.unmount());
|
|
22
|
+
expect(usePreviewIframeStore.getState().iframe).toBeNull();
|
|
23
|
+
|
|
24
|
+
seen.length = 0;
|
|
25
|
+
const second = createRoot(document.createElement("div"));
|
|
26
|
+
act(() => second.render(<Owner />));
|
|
27
|
+
expect(seen).toEqual([null]);
|
|
28
|
+
act(() => second.unmount());
|
|
29
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
|
+
|
|
4
|
+
interface PreviewIframeState {
|
|
5
|
+
iframe: HTMLIFrameElement | null;
|
|
6
|
+
setIframe: (iframe: HTMLIFrameElement | null) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The one owner of "the live preview iframe element"; a promoted reload replaces it. */
|
|
10
|
+
export const usePreviewIframeStore = create<PreviewIframeState>((set) => ({
|
|
11
|
+
iframe: null,
|
|
12
|
+
// Return the same state reference on a no-op set so zustand skips the
|
|
13
|
+
// notify (Object.is check in setState) instead of merging a new object
|
|
14
|
+
// every time a caller re-pushes the current value.
|
|
15
|
+
setIframe: (iframe) => set((state) => (Object.is(state.iframe, iframe) ? state : { iframe })),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
/** Subscribe an effect to the live iframe by listing the result in its dependencies. */
|
|
19
|
+
export const useLivePreviewIframe = () => usePreviewIframeStore((s) => s.iframe);
|
|
20
|
+
|
|
21
|
+
/** The owner's hook: reads the live iframe and clears it on unmount so a remount starts from null. */
|
|
22
|
+
export function useOwnPreviewIframe() {
|
|
23
|
+
useMountEffect(() => () => usePreviewIframeStore.getState().setIframe(null));
|
|
24
|
+
return useLivePreviewIframe();
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
// Imports the render queue the way a host app does: by package name, through the declared exports.
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
RenderQueue,
|
|
8
|
+
getPersistedRenderSettings,
|
|
9
|
+
persistRenderSettings,
|
|
10
|
+
useRenderQueue,
|
|
11
|
+
type FfmpegStatus,
|
|
12
|
+
type RenderJob,
|
|
13
|
+
} from "@hyperframes/studio";
|
|
14
|
+
|
|
15
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
16
|
+
|
|
17
|
+
describe("render queue package exports", () => {
|
|
18
|
+
it("exposes the hook and settings helpers", () => {
|
|
19
|
+
expect(typeof useRenderQueue).toBe("function");
|
|
20
|
+
persistRenderSettings("webm", "high", 30);
|
|
21
|
+
expect(getPersistedRenderSettings()).toMatchObject({
|
|
22
|
+
format: "webm",
|
|
23
|
+
quality: "high",
|
|
24
|
+
fps: 30,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("mounts RenderQueue with a host-supplied job", async () => {
|
|
29
|
+
const job: RenderJob = {
|
|
30
|
+
id: "j1",
|
|
31
|
+
status: "complete",
|
|
32
|
+
progress: 100,
|
|
33
|
+
filename: "host-render.mp4",
|
|
34
|
+
createdAt: 0,
|
|
35
|
+
};
|
|
36
|
+
const ffmpeg: FfmpegStatus = { ok: true };
|
|
37
|
+
const el = document.createElement("div");
|
|
38
|
+
document.body.append(el);
|
|
39
|
+
const root = createRoot(el);
|
|
40
|
+
await act(async () => {
|
|
41
|
+
root.render(
|
|
42
|
+
<RenderQueue
|
|
43
|
+
jobs={[job]}
|
|
44
|
+
projectId="p"
|
|
45
|
+
onDelete={vi.fn()}
|
|
46
|
+
onClearCompleted={vi.fn()}
|
|
47
|
+
onStartRender={vi.fn()}
|
|
48
|
+
isRendering={false}
|
|
49
|
+
ffmpeg={ffmpeg}
|
|
50
|
+
ffmpegChecking={false}
|
|
51
|
+
onRecheckFfmpeg={vi.fn()}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
expect(el.textContent).toContain("host-render.mp4");
|
|
56
|
+
await act(async () => root.unmount());
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractClassCandidates } from "./classCandidates";
|
|
3
|
+
|
|
4
|
+
function bases(source: string): string[] {
|
|
5
|
+
return extractClassCandidates(source)
|
|
6
|
+
.map((candidate) => candidate.base)
|
|
7
|
+
.sort();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("extractClassCandidates", () => {
|
|
11
|
+
it("reads a plain className attribute", () => {
|
|
12
|
+
expect(bases(`<div className="text-center border-dashed" />`)).toEqual([
|
|
13
|
+
"border-dashed",
|
|
14
|
+
"text-center",
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("reads every string a class-building call is given", () => {
|
|
19
|
+
const source = `<div className={cn("flex", open && "bg-surface", { "text-accent": on })} />`;
|
|
20
|
+
|
|
21
|
+
expect(bases(source)).toEqual(["bg-surface", "flex", "text-accent"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("reads a class map that never touches a className literal", () => {
|
|
25
|
+
// The shape Studio's primitives use: the utility only reaches markup
|
|
26
|
+
// through a lookup, so an attribute-only reader would miss it entirely.
|
|
27
|
+
const source = `const sizeStyles: Record<ButtonSize, string> = {
|
|
28
|
+
sm: "h-7 px-2.5 rounded-button",
|
|
29
|
+
};`;
|
|
30
|
+
|
|
31
|
+
expect(bases(source)).toEqual(["h-7", "px-2.5", "rounded-button"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("reads a class map whose name says nothing about classes", () => {
|
|
35
|
+
// The defect this replaced: candidate-ness was decided by the binding's
|
|
36
|
+
// name, so the same classes were gated in `sizeStyles` and invisible in
|
|
37
|
+
// `buttonSizes`. What makes them classes is where they are used.
|
|
38
|
+
const source = `const fooSizes = { md: "h-ctl rounded-hologram" };
|
|
39
|
+
const Foo = ({ size }) => <span className={cn(fooSizes[size])} />;`;
|
|
40
|
+
|
|
41
|
+
expect(bases(source)).toEqual(["h-ctl", "rounded-hologram"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("follows an identifier that contains a dollar sign", () => {
|
|
45
|
+
const source = `const $cls = "flex-none";\nconst a = <div className={$cls} />;`;
|
|
46
|
+
|
|
47
|
+
expect(bases(source)).toEqual(["flex-none"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does not read a lookup key's default value as a class", () => {
|
|
51
|
+
// `variant` is the key, not the class list, so the default it falls back
|
|
52
|
+
// to is a map entry name and never reaches markup.
|
|
53
|
+
const source = `const map = { ghost: "bg-transparent" };
|
|
54
|
+
const Foo = ({ variant = "ghost" }) => <span className={map[variant]} />;`;
|
|
55
|
+
|
|
56
|
+
expect(bases(source)).toEqual(["bg-transparent"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves a lookup table alone when nothing uses it as a class", () => {
|
|
60
|
+
const source = `const labels = { md: "Medium size" };
|
|
61
|
+
const Foo = () => <span title={labels.md} />;`;
|
|
62
|
+
|
|
63
|
+
expect(bases(source)).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("drops the template chunk that touches an interpolation", () => {
|
|
67
|
+
expect(bases("<div className={`w-${i} flex`} />")).toEqual(["flex"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("keeps a chunk that whitespace separates from the interpolation", () => {
|
|
71
|
+
expect(bases("<div className={`${base} flex `} />")).toEqual(["flex"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("strips variant prefixes, including bracketed ones", () => {
|
|
75
|
+
const source = `<div className="hover:bg-surface/50 md:group-data-[open]:text-accent" />`;
|
|
76
|
+
|
|
77
|
+
expect(bases(source)).toEqual(["bg-surface/50", "text-accent"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("flags bracketed arbitrary values so the sweep can count them", () => {
|
|
81
|
+
const found = extractClassCandidates(`<div className="text-[11px] text-center" />`);
|
|
82
|
+
|
|
83
|
+
expect(found.filter((candidate) => candidate.arbitrary).map((c) => c.raw)).toEqual([
|
|
84
|
+
"text-[11px]",
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("ignores strings that are not class lists", () => {
|
|
89
|
+
const source = `const styles = getComputedStyle(el);
|
|
90
|
+
const label = "Export video";
|
|
91
|
+
log("failed to open the project file");`;
|
|
92
|
+
|
|
93
|
+
expect(bases(source)).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not read past the end of a class region", () => {
|
|
97
|
+
const source = `<div className="flex" title="Export video" />`;
|
|
98
|
+
|
|
99
|
+
expect(bases(source)).toEqual(["flex"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("survives an unterminated region without hanging", () => {
|
|
103
|
+
expect(bases(`<div className={cn("flex",`)).toEqual(["flex"]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Pulls Tailwind class candidates out of a TS/TSX file: only strings unambiguously used as class lists
|
|
2
|
+
// (className/class attributes, cn/clsx/cva-style calls, `*class` properties, identifiers those use,
|
|
3
|
+
// `*Styles`/`*Classes` bindings). Bare strings are ignored so prose and paths never become "classes".
|
|
4
|
+
// Pure: text in, candidates out.
|
|
5
|
+
|
|
6
|
+
import { escapeRegex } from "../utils/sourcePatcher";
|
|
7
|
+
|
|
8
|
+
/** A class candidate and where it came from. */
|
|
9
|
+
export interface ClassCandidate {
|
|
10
|
+
/** The utility with its variant prefixes removed (`hover:bg-x` -> `bg-x`). */
|
|
11
|
+
readonly base: string;
|
|
12
|
+
/** The token exactly as it was written, for the failure message. */
|
|
13
|
+
readonly raw: string;
|
|
14
|
+
/** True when the utility carries a bracketed arbitrary value. */
|
|
15
|
+
readonly arbitrary: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const OPEN: Record<string, string> = { "{": "}", "(": ")", "[": "]" };
|
|
19
|
+
|
|
20
|
+
// Index of the last char of the non-code span (string, template, line or block comment) starting at
|
|
21
|
+
// `i`, else -1. Known limit: a regex literal containing a quote reads as a string; the cost is a
|
|
22
|
+
// spurious candidate, which the gate reports.
|
|
23
|
+
function endOfSpan(text: string, i: number): number {
|
|
24
|
+
const ch = text[i];
|
|
25
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
26
|
+
return Math.min(endOfString(text, i), text.length - 1);
|
|
27
|
+
}
|
|
28
|
+
if (ch !== "/") return -1;
|
|
29
|
+
if (text[i + 1] === "/") {
|
|
30
|
+
const end = text.indexOf("\n", i);
|
|
31
|
+
return end === -1 ? text.length - 1 : end - 1;
|
|
32
|
+
}
|
|
33
|
+
if (text[i + 1] === "*") {
|
|
34
|
+
const end = text.indexOf("*/", i + 2);
|
|
35
|
+
return end === -1 ? text.length - 1 : end + 1;
|
|
36
|
+
}
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Index just past the region starting at `start` (an opening bracket), skipping strings, templates
|
|
41
|
+
// and comments; `text.length` when it never closes.
|
|
42
|
+
function endOfRegion(text: string, start: number): number {
|
|
43
|
+
const stack: string[] = [];
|
|
44
|
+
for (let i = start; i < text.length; i += 1) {
|
|
45
|
+
const span = endOfSpan(text, i);
|
|
46
|
+
if (span >= i) {
|
|
47
|
+
i = span;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const ch = text[i];
|
|
51
|
+
if (OPEN[ch]) {
|
|
52
|
+
stack.push(OPEN[ch]);
|
|
53
|
+
} else if (stack.length > 0 && ch === stack[stack.length - 1]) {
|
|
54
|
+
stack.pop();
|
|
55
|
+
if (stack.length === 0) return i + 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return text.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Index of the closing quote of the string that opens at `start`. */
|
|
62
|
+
function endOfString(text: string, start: number): number {
|
|
63
|
+
const quote = text[start];
|
|
64
|
+
for (let i = start + 1; i < text.length; i += 1) {
|
|
65
|
+
if (text[i] === "\\") {
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (text[i] === quote) return i;
|
|
70
|
+
if (quote === "`" && text[i] === "$" && text[i + 1] === "{") {
|
|
71
|
+
i = endOfRegion(text, i + 1) - 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return text.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Splits a template literal body into safe chunks. A chunk touching an interpolation is dropped at
|
|
78
|
+
// that edge (`w-${size}` claims no class `w-`) unless whitespace separates it from the hole.
|
|
79
|
+
function templateChunks(body: string): string[] {
|
|
80
|
+
const chunks: string[] = [];
|
|
81
|
+
let cursor = 0;
|
|
82
|
+
let precededByHole = false;
|
|
83
|
+
while (cursor <= body.length) {
|
|
84
|
+
const hole = body.indexOf("${", cursor);
|
|
85
|
+
const end = hole === -1 ? body.length : hole;
|
|
86
|
+
let chunk = body.slice(cursor, end);
|
|
87
|
+
if (precededByHole && !/^\s/.test(chunk)) chunk = chunk.replace(/^\S+/, "");
|
|
88
|
+
if (hole !== -1 && !/\s$/.test(chunk)) chunk = chunk.replace(/\S+$/, "");
|
|
89
|
+
chunks.push(chunk);
|
|
90
|
+
if (hole === -1) break;
|
|
91
|
+
cursor = endOfRegion(body, hole + 1);
|
|
92
|
+
precededByHole = true;
|
|
93
|
+
}
|
|
94
|
+
return chunks;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A same-length copy with comment and string bodies blanked, so offsets line up. Anchors match
|
|
98
|
+
// against it to keep out prose (one apostrophe in a comment opens a string that swallows the rest)
|
|
99
|
+
// and generated code (`captions/generator.ts` writes `x.className = 'word'` into a string).
|
|
100
|
+
function maskLiterals(text: string): string {
|
|
101
|
+
let out = "";
|
|
102
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
103
|
+
const span = endOfSpan(text, i);
|
|
104
|
+
if (span < i) {
|
|
105
|
+
out += text[i];
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// A comment goes entirely, newlines kept so line numbers survive. A string
|
|
109
|
+
// keeps its quotes, so the region scan still finds where it ends.
|
|
110
|
+
const body = " ".repeat(span - i - 1);
|
|
111
|
+
out +=
|
|
112
|
+
text[i] === "/"
|
|
113
|
+
? text.slice(i, span + 1).replace(/[^\n]/g, " ")
|
|
114
|
+
: text[i] + body + text[span];
|
|
115
|
+
i = span;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// `activeTool === "razor" ? a : b` inside a `className=` region: the operand
|
|
121
|
+
// of a comparison is a value being tested, never a class list.
|
|
122
|
+
const COMPARISON = /(?:[=!]==?|\bcase)\s*$/;
|
|
123
|
+
// A class list never carries a declaration, so `position: absolute; ...` in a
|
|
124
|
+
// constant called `visualStyles` is a stylesheet, not classes.
|
|
125
|
+
const CSS_TEXT = /[a-z-]+\s*:\s*[^;]+;/;
|
|
126
|
+
// The calls whose arguments are class lists.
|
|
127
|
+
const CLASS_BUILDER = /^(?:cn|clsx|classnames|classNames|twMerge|twJoin|cva|tv)$/;
|
|
128
|
+
|
|
129
|
+
// Written with escapes, not literal quotes: `maskLiterals` reads a quote in
|
|
130
|
+
// a regular-expression literal as the start of a string, and this file is
|
|
131
|
+
// one of the files the gate scans.
|
|
132
|
+
const CLOSE = /[)\]}]/;
|
|
133
|
+
const QUOTE = /[\u0022\u0027\u0060]/;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A string is a class list unless it is being compared against or is a sheet
|
|
137
|
+
* of CSS text.
|
|
138
|
+
*/
|
|
139
|
+
function isClassList(mask: string, start: number, i: number, body: string): boolean {
|
|
140
|
+
return !COMPARISON.test(mask.slice(Math.max(start, i - 8), i)) && !CSS_TEXT.test(body);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Does a string inside the bracket that opens at `i` hold classes? A bracket opened by a
|
|
145
|
+
* non-class-builder name closes the door: `T[f.inlineStyles["font-weight"]]` has a lookup key.
|
|
146
|
+
*/
|
|
147
|
+
function opensClassContext(mask: string, from: number, i: number): boolean {
|
|
148
|
+
const name = /([A-Za-z_$][\w$]*)\s*$/.exec(mask.slice(Math.max(from, i - 40), i))?.[1];
|
|
149
|
+
return name === undefined || CLASS_BUILDER.test(name);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Every string and template body inside a source region. Where the strings are
|
|
154
|
+
* is decided on the mask; what they contain is read from the real text.
|
|
155
|
+
*/
|
|
156
|
+
function stringsIn(text: string, mask: string, start: number, end: number): string[] {
|
|
157
|
+
const found: string[] = [];
|
|
158
|
+
const classy: boolean[] = [];
|
|
159
|
+
for (let i = start; i < end; i += 1) {
|
|
160
|
+
const ch = mask[i];
|
|
161
|
+
if (OPEN[ch]) classy.push(opensClassContext(mask, start, i));
|
|
162
|
+
else if (CLOSE.test(ch)) classy.pop();
|
|
163
|
+
else if (QUOTE.test(ch)) {
|
|
164
|
+
const close = endOfString(mask, i);
|
|
165
|
+
const body = text.slice(i + 1, close);
|
|
166
|
+
if (classy.at(-1) !== false && isClassList(mask, start, i, body)) {
|
|
167
|
+
found.push(...(ch === "`" ? templateChunks(body) : [body]));
|
|
168
|
+
}
|
|
169
|
+
i = close;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Split on `:` that sits outside brackets, so `[&:hover]:x` yields `x`. */
|
|
176
|
+
function stripVariants(token: string): string {
|
|
177
|
+
let depth = 0;
|
|
178
|
+
let last = 0;
|
|
179
|
+
for (let i = 0; i < token.length; i += 1) {
|
|
180
|
+
const ch = token[i];
|
|
181
|
+
if (ch === "[" || ch === "(") depth += 1;
|
|
182
|
+
else if (ch === "]" || ch === ")") depth -= 1;
|
|
183
|
+
else if (ch === ":" && depth === 0) last = i + 1;
|
|
184
|
+
}
|
|
185
|
+
return token.slice(last);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// A utility starts with a letter, a digit, `-` (negative), `@` (container
|
|
189
|
+
// query) or `[` (fully arbitrary), and never contains a quote or a backslash.
|
|
190
|
+
// The quotes are written as escapes so this file can be read by its own
|
|
191
|
+
// comment stripper, which treats a quote in a regex literal as a string.
|
|
192
|
+
const TOKEN = /^[-@[a-z0-9][^\s\u0022\u0027\u0060\\]*$/;
|
|
193
|
+
|
|
194
|
+
// Class contexts: each anchor's balanced region is read for strings. The first `CLASS_POSITIONS` are
|
|
195
|
+
// places a class lands, so identifiers used inside them are followed to their bindings.
|
|
196
|
+
const ANCHORS = [
|
|
197
|
+
// className={...} / className="..." / class="..."
|
|
198
|
+
/\bclass(?:Name)?\s*=\s*/g,
|
|
199
|
+
// cn("..."), clsx(...), twMerge(...), cva(...)
|
|
200
|
+
/\b(?:cn|clsx|classnames|classNames|twMerge|twJoin|cva|tv)\s*\(/g,
|
|
201
|
+
// { className: "...", itemClass: "..." }
|
|
202
|
+
/\b\w*[Cc]lass(?:Names?)?\s*:\s*/g,
|
|
203
|
+
// const sizeStyles: Record<Size, string> = { ... } / baseClasses = "..." (exported maps used elsewhere)
|
|
204
|
+
// Prefix and plural both required: bare `style=` is the JSX attribute, `fxPresetStyle` is CSS text.
|
|
205
|
+
// The optional group is a type annotation; without it the gap spans `computedStyles["z-index"] !== "auto"`.
|
|
206
|
+
/\b\w+(?:Styles|Classes|ClassNames?)\b(?:\s*:[^=;{}()[\]]*)?\s*=(?!=)\s*/g,
|
|
207
|
+
] as const;
|
|
208
|
+
|
|
209
|
+
const CLASS_POSITIONS = 3;
|
|
210
|
+
|
|
211
|
+
/** Bare identifiers in a region of the mask; property accesses excluded. */
|
|
212
|
+
const REFERENCE = /(?<![.\w$])[A-Za-z_$][\w$]*/g;
|
|
213
|
+
|
|
214
|
+
// A subscript, i.e. the `[variant]` of `variantStyles[variant]`. In
|
|
215
|
+
// `cn(variantStyles[variant])` the class list is the map; the key is a value
|
|
216
|
+
// being looked up, and `variant = "secondary"` is a default, not a class.
|
|
217
|
+
const SUBSCRIPT = /(?<=[\w$)\]])\s*\[[^[\]]*\]/g;
|
|
218
|
+
|
|
219
|
+
// End of the bracketed region or string at `at`, else -1: `const styles = getComputedStyle(el)`
|
|
220
|
+
// matches the binding-name anchor but is not a class list.
|
|
221
|
+
function regionAfter(mask: string, at: number): number {
|
|
222
|
+
if (OPEN[mask[at]]) return endOfRegion(mask, at);
|
|
223
|
+
if (QUOTE.test(mask[at])) return endOfString(mask, at) + 1;
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Split one class list into candidates, keeping the first spelling of each. */
|
|
228
|
+
function collect(byRaw: Map<string, ClassCandidate>, list: string): void {
|
|
229
|
+
for (const raw of list.split(/\s+/)) {
|
|
230
|
+
if (raw === "" || !TOKEN.test(raw) || byRaw.has(raw)) continue;
|
|
231
|
+
const base = stripVariants(raw);
|
|
232
|
+
if (base === "") continue;
|
|
233
|
+
byRaw.set(raw, { base, raw, arbitrary: base.includes("[") });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Every `[start, end)` an anchor opens. `lastIndex` jumps past each region so a nested `cn()`
|
|
238
|
+
// inside a `className=` is not read twice.
|
|
239
|
+
function regionsAfter(mask: string, anchor: RegExp): [number, number][] {
|
|
240
|
+
const pattern = new RegExp(anchor.source, "g");
|
|
241
|
+
const regions: [number, number][] = [];
|
|
242
|
+
let match: RegExpExecArray | null;
|
|
243
|
+
while ((match = pattern.exec(mask)) !== null) {
|
|
244
|
+
const start = match.index + match[0].length - (match[0].endsWith("(") ? 1 : 0);
|
|
245
|
+
const end = regionAfter(mask, start);
|
|
246
|
+
if (end <= start) continue;
|
|
247
|
+
regions.push([start, end]);
|
|
248
|
+
pattern.lastIndex = Math.max(pattern.lastIndex, end);
|
|
249
|
+
}
|
|
250
|
+
return regions;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Every string bound to `name` by an assignment in this file. A name with no literal binding
|
|
254
|
+
// (parameter, import, call result) yields nothing, so an over-broad reference scan is safe.
|
|
255
|
+
function boundStrings(text: string, mask: string, name: string): string[] {
|
|
256
|
+
const anchor = new RegExp(
|
|
257
|
+
`(?<![\\w$])${escapeRegex(name)}(?![\\w$])(?:\\s*:[^=;{}()[\\]]*)?\\s*=(?!=)\\s*`,
|
|
258
|
+
);
|
|
259
|
+
return regionsAfter(mask, anchor).flatMap(([start, end]) => stringsIn(text, mask, start, end));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Every class candidate claimed by one source file. */
|
|
263
|
+
export function extractClassCandidates(source: string): ClassCandidate[] {
|
|
264
|
+
// Anchors and region bounds come from the mask (code only); the strings
|
|
265
|
+
// themselves are read back out of the real text.
|
|
266
|
+
const mask = maskLiterals(source);
|
|
267
|
+
const byRaw = new Map<string, ClassCandidate>();
|
|
268
|
+
const referenced = new Set<string>();
|
|
269
|
+
for (const [index, anchor] of ANCHORS.entries()) {
|
|
270
|
+
for (const [start, end] of regionsAfter(mask, anchor)) {
|
|
271
|
+
for (const text of stringsIn(source, mask, start, end)) collect(byRaw, text);
|
|
272
|
+
if (index >= CLASS_POSITIONS) continue;
|
|
273
|
+
const used = mask.slice(start, end).replace(SUBSCRIPT, "");
|
|
274
|
+
for (const [name] of used.matchAll(REFERENCE)) referenced.add(name);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const name of referenced) {
|
|
278
|
+
for (const text of boundStrings(source, mask, name)) collect(byRaw, text);
|
|
279
|
+
}
|
|
280
|
+
return [...byRaw.values()];
|
|
281
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"total": 394,
|
|
3
|
+
"files": {
|
|
4
|
+
"packages/studio/src/captions/components/CaptionOverlay.tsx": 5,
|
|
5
|
+
"packages/studio/src/captions/components/CaptionTimeline.tsx": 11,
|
|
6
|
+
"packages/studio/src/captions/generator.ts": 1,
|
|
7
|
+
"packages/studio/src/captions/parser.ts": 1,
|
|
8
|
+
"packages/studio/src/captions/types.ts": 4,
|
|
9
|
+
"packages/studio/src/components/StudioToast.tsx": 10,
|
|
10
|
+
"packages/studio/src/components/TimelineToolbar.tsx": 2,
|
|
11
|
+
"packages/studio/src/components/editor/ArcPathControls.tsx": 1,
|
|
12
|
+
"packages/studio/src/components/editor/BorderRadiusEditor.tsx": 10,
|
|
13
|
+
"packages/studio/src/components/editor/DomEditCropHandles.tsx": 1,
|
|
14
|
+
"packages/studio/src/components/editor/EaseCurveSection.tsx": 2,
|
|
15
|
+
"packages/studio/src/components/editor/FileTree.tsx": 2,
|
|
16
|
+
"packages/studio/src/components/editor/FileTreeIcons.tsx": 17,
|
|
17
|
+
"packages/studio/src/components/editor/FileTreeNodes.tsx": 4,
|
|
18
|
+
"packages/studio/src/components/editor/GestureTrailOverlay.tsx": 1,
|
|
19
|
+
"packages/studio/src/components/editor/GridOverlay.tsx": 4,
|
|
20
|
+
"packages/studio/src/components/editor/InlineTextToolbar.tsx": 2,
|
|
21
|
+
"packages/studio/src/components/editor/KeyframeDiamond.tsx": 2,
|
|
22
|
+
"packages/studio/src/components/editor/KeyframeNavigation.tsx": 2,
|
|
23
|
+
"packages/studio/src/components/editor/MotionPathNode.tsx": 5,
|
|
24
|
+
"packages/studio/src/components/editor/SnapGuideOverlay.tsx": 3,
|
|
25
|
+
"packages/studio/src/components/editor/TimelineFxPopover.tsx": 1,
|
|
26
|
+
"packages/studio/src/components/editor/Transform3DCube.tsx": 6,
|
|
27
|
+
"packages/studio/src/components/editor/colorValue.ts": 3,
|
|
28
|
+
"packages/studio/src/components/editor/domEditingElement.ts": 1,
|
|
29
|
+
"packages/studio/src/components/editor/easeCurveSvg.tsx": 2,
|
|
30
|
+
"packages/studio/src/components/editor/gradientValue.ts": 6,
|
|
31
|
+
"packages/studio/src/components/editor/panelTokens.ts": 4,
|
|
32
|
+
"packages/studio/src/components/editor/propertyPanelColor.tsx": 7,
|
|
33
|
+
"packages/studio/src/components/editor/propertyPanelColorCurveGraph.tsx": 17,
|
|
34
|
+
"packages/studio/src/components/editor/propertyPanelColorScopes.tsx": 8,
|
|
35
|
+
"packages/studio/src/components/editor/propertyPanelColorWheels.tsx": 8,
|
|
36
|
+
"packages/studio/src/components/editor/propertyPanelFlatEffectsSection.tsx": 5,
|
|
37
|
+
"packages/studio/src/components/editor/propertyPanelFlatStyleSections.tsx": 2,
|
|
38
|
+
"packages/studio/src/components/editor/propertyPanelFxCarveModule.tsx": 1,
|
|
39
|
+
"packages/studio/src/components/editor/propertyPanelFxFamily.ts": 1,
|
|
40
|
+
"packages/studio/src/components/editor/propertyPanelFxPresetStyle.ts": 24,
|
|
41
|
+
"packages/studio/src/components/editor/propertyPanelHelpers.ts": 4,
|
|
42
|
+
"packages/studio/src/components/editor/propertyPanelPrimitives.tsx": 1,
|
|
43
|
+
"packages/studio/src/components/editor/propertyPanelSections.tsx": 1,
|
|
44
|
+
"packages/studio/src/components/editor/propertyPanelStyleSections.tsx": 3,
|
|
45
|
+
"packages/studio/src/components/editor/transform3dProjection.ts": 3,
|
|
46
|
+
"packages/studio/src/components/feedback/StudioFeedbackCard.tsx": 10,
|
|
47
|
+
"packages/studio/src/components/panels/VariablesBindElement.tsx": 2,
|
|
48
|
+
"packages/studio/src/components/panels/VariablesValueControls.tsx": 2,
|
|
49
|
+
"packages/studio/src/components/sidebar/AudioRow.tsx": 2,
|
|
50
|
+
"packages/studio/src/components/ui/HyperframesMark.tsx": 4,
|
|
51
|
+
"packages/studio/src/hooks/useInlineTextEdit.ts": 1,
|
|
52
|
+
"packages/studio/src/player/components/AudioWaveform.tsx": 6,
|
|
53
|
+
"packages/studio/src/player/components/BeatStrip.tsx": 6,
|
|
54
|
+
"packages/studio/src/player/components/CompositionThumbnail.tsx": 2,
|
|
55
|
+
"packages/studio/src/player/components/ImageThumbnail.tsx": 6,
|
|
56
|
+
"packages/studio/src/player/components/LayerDisclosureRow.tsx": 3,
|
|
57
|
+
"packages/studio/src/player/components/Player.tsx": 2,
|
|
58
|
+
"packages/studio/src/player/components/PlayerControls.tsx": 1,
|
|
59
|
+
"packages/studio/src/player/components/PlayheadIndicator.tsx": 3,
|
|
60
|
+
"packages/studio/src/player/components/ShortcutsPanel.tsx": 7,
|
|
61
|
+
"packages/studio/src/player/components/SpeedMenu.tsx": 2,
|
|
62
|
+
"packages/studio/src/player/components/TimelineAutomationLane.tsx": 4,
|
|
63
|
+
"packages/studio/src/player/components/TimelineCanvas.tsx": 13,
|
|
64
|
+
"packages/studio/src/player/components/TimelineClip.tsx": 2,
|
|
65
|
+
"packages/studio/src/player/components/TimelineClipDiamonds.tsx": 2,
|
|
66
|
+
"packages/studio/src/player/components/TimelineDiamondConnectors.tsx": 2,
|
|
67
|
+
"packages/studio/src/player/components/TimelineEmptyState.tsx": 1,
|
|
68
|
+
"packages/studio/src/player/components/TimelineFxButton.tsx": 8,
|
|
69
|
+
"packages/studio/src/player/components/TimelineGroupHeader.tsx": 6,
|
|
70
|
+
"packages/studio/src/player/components/TimelineGroupLaneLabels.tsx": 1,
|
|
71
|
+
"packages/studio/src/player/components/TimelineGroupRow.tsx": 1,
|
|
72
|
+
"packages/studio/src/player/components/TimelineRazorInteraction.tsx": 1,
|
|
73
|
+
"packages/studio/src/player/components/TimelineRuler.tsx": 1,
|
|
74
|
+
"packages/studio/src/player/components/TimelineShortcutHint.tsx": 2,
|
|
75
|
+
"packages/studio/src/player/components/TimelineTrackHeader.tsx": 2,
|
|
76
|
+
"packages/studio/src/player/components/TimelineTrackPlainHeader.tsx": 2,
|
|
77
|
+
"packages/studio/src/player/components/VideoThumbnail.tsx": 6,
|
|
78
|
+
"packages/studio/src/player/components/timelineTheme.ts": 5,
|
|
79
|
+
"packages/studio/src/player/components/trackHeaderLabelRows.tsx": 6,
|
|
80
|
+
"packages/studio/src/styles/studio.css": 70,
|
|
81
|
+
"packages/studio/src/telemetry/client.ts": 1,
|
|
82
|
+
"packages/studio/src/webmcp/webmcpTestUtils.ts": 1
|
|
83
|
+
}
|
|
84
|
+
}
|