@hyperframes/studio 0.8.105 → 0.8.106
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-tnfCiVdR.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/{index-DEDhPmPD.js → index-CMiYVGv5.js} +202 -202
- package/dist/assets/{index-zpe332hb.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-Ca6LZlVb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-QADHJFDF.js → chunk-ICTZJPQQ.js} +2 -1
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +6601 -6022
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -9
- package/src/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/StudioHeader.dom.test.tsx +31 -9
- package/src/components/StudioHeader.tsx +4 -1
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/editor/DomEditOverlay.tsx +37 -5
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +33 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +69 -46
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +99 -19
- package/src/hooks/useClipboard.ts +145 -124
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +14 -6
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/studio.css +6 -0
- package/src/styles/theme.css +10 -0
- package/src/utils/clipboardPayload.ts +10 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-Cre9X4RS.css +0 -1
- package/dist/chunk-QADHJFDF.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.106",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"@codemirror/language": "^6.12.2",
|
|
51
51
|
"@codemirror/search": "^6.6.0",
|
|
52
52
|
"@codemirror/state": "^6.6.0",
|
|
53
|
-
"@codemirror/theme-one-dark": "^6.1.2",
|
|
54
53
|
"@codemirror/view": "6.40.0",
|
|
54
|
+
"@lezer/highlight": "^1.2.3",
|
|
55
55
|
"@mcp-b/global": "^5.0.1",
|
|
56
56
|
"@phosphor-icons/react": "^2.1.10",
|
|
57
57
|
"@tanstack/react-virtual": "^3.14.13",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/core": "0.8.
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/core": "0.8.106",
|
|
64
|
+
"@hyperframes/parsers": "0.8.106",
|
|
65
|
+
"@hyperframes/sdk": "0.8.106",
|
|
66
|
+
"@hyperframes/player": "0.8.106",
|
|
67
|
+
"@hyperframes/shader-transitions": "0.8.106",
|
|
68
|
+
"@hyperframes/studio-server": "0.8.106"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.106"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -38,6 +38,12 @@ describe("shouldHandleCaptionNudgeKey", () => {
|
|
|
38
38
|
expect(shouldHandleCaptionNudgeKey(mockKeyboardEvent("KeyL"))).toBe(false);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
it("ignores arrows a focused slider owns", () => {
|
|
42
|
+
const slider = document.createElement("div");
|
|
43
|
+
slider.setAttribute("role", "slider");
|
|
44
|
+
expect(shouldHandleCaptionNudgeKey(mockKeyboardEvent("ArrowUp"), slider)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
41
47
|
it("ignores arrows when the event target is an editable element", () => {
|
|
42
48
|
const input = document.createElement("input");
|
|
43
49
|
const textarea = document.createElement("textarea");
|
package/src/captions/keyboard.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isEditableTarget } from "../utils/timelineDiscovery";
|
|
2
|
+
import { ownsPlainKeys } from "../utils/typingTarget";
|
|
2
3
|
|
|
3
4
|
const CAPTION_NUDGE_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]);
|
|
4
5
|
|
|
@@ -9,7 +10,7 @@ export function shouldHandleCaptionNudgeKey(
|
|
|
9
10
|
target?: EventTarget | null,
|
|
10
11
|
): boolean {
|
|
11
12
|
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
|
12
|
-
if (target != null &&
|
|
13
|
+
if (target != null && ownsPlainKeys(target)) return false;
|
|
13
14
|
return CAPTION_NUDGE_KEYS.has(event.key);
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -42,6 +42,7 @@ vi.mock("../contexts/PanelLayoutContext", () => ({
|
|
|
42
42
|
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
43
43
|
|
|
44
44
|
const { StudioHeader } = await import("./StudioHeader");
|
|
45
|
+
const { ShowThemeToggle } = await import("./ThemeToggle");
|
|
45
46
|
|
|
46
47
|
let mounted: { root: Root; host: HTMLElement } | null = null;
|
|
47
48
|
|
|
@@ -54,6 +55,7 @@ beforeEach(() => {
|
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
afterEach(() => {
|
|
58
|
+
delete document.documentElement.dataset.theme;
|
|
57
59
|
if (!mounted) return;
|
|
58
60
|
const { root, host } = mounted;
|
|
59
61
|
mounted = null;
|
|
@@ -61,21 +63,30 @@ afterEach(() => {
|
|
|
61
63
|
host.remove();
|
|
62
64
|
});
|
|
63
65
|
|
|
64
|
-
function mount(
|
|
66
|
+
function mount(
|
|
67
|
+
props: { inspectorButtonActive?: boolean; themeToggle?: boolean } = {},
|
|
68
|
+
): HTMLElement {
|
|
65
69
|
const host = document.createElement("div");
|
|
66
70
|
document.body.append(host);
|
|
67
71
|
const root = createRoot(host);
|
|
68
72
|
mounted = { root, host };
|
|
73
|
+
const header = (
|
|
74
|
+
<StudioHeader
|
|
75
|
+
captureFrameHref="blob:frame"
|
|
76
|
+
captureFrameFilename="frame.png"
|
|
77
|
+
handleCaptureFrameClick={vi.fn()}
|
|
78
|
+
refreshCaptureFrameTime={vi.fn()}
|
|
79
|
+
inspectorButtonActive={props.inspectorButtonActive ?? false}
|
|
80
|
+
inspectorPanelActive={false}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
69
83
|
act(() =>
|
|
70
84
|
root.render(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
inspectorButtonActive={props.inspectorButtonActive ?? false}
|
|
77
|
-
inspectorPanelActive={false}
|
|
78
|
-
/>,
|
|
85
|
+
props.themeToggle ? (
|
|
86
|
+
<ShowThemeToggle.Provider value>{header}</ShowThemeToggle.Provider>
|
|
87
|
+
) : (
|
|
88
|
+
header
|
|
89
|
+
),
|
|
79
90
|
),
|
|
80
91
|
);
|
|
81
92
|
return host;
|
|
@@ -179,3 +190,14 @@ it("classifies the new header controls for the hotkey filters as the old ones we
|
|
|
179
190
|
expect(shouldIgnorePlaybackShortcutTarget(el), el.tagName).toBe(true);
|
|
180
191
|
}
|
|
181
192
|
});
|
|
193
|
+
|
|
194
|
+
it("shows no theme toggle and leaves the host's data-theme alone when embedded", () => {
|
|
195
|
+
document.documentElement.dataset.theme = "host";
|
|
196
|
+
const host = mount();
|
|
197
|
+
expect(host.querySelector('[aria-label^="Switch to"]')).toBeNull();
|
|
198
|
+
expect(document.documentElement.dataset.theme).toBe("host");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("shows the theme toggle in Studio's own app", () => {
|
|
202
|
+
expect(mount({ themeToggle: true }).querySelector('[aria-label^="Switch to"]')).not.toBeNull();
|
|
203
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useContext, type MouseEvent } from "react";
|
|
2
2
|
import { Camera } from "../icons/SystemIcons";
|
|
3
3
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
4
4
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
@@ -7,6 +7,7 @@ import { Button, buttonBase, buttonSizes, buttonVariants, cn, Tooltip } from "./
|
|
|
7
7
|
import { Dock } from "./dock/Dock";
|
|
8
8
|
import { InspectorIcon } from "./icons/InspectorIcon";
|
|
9
9
|
import { HyperframesLogo } from "./ui/HyperframesLogo";
|
|
10
|
+
import { ShowThemeToggle, ThemeToggle } from "./ThemeToggle";
|
|
10
11
|
|
|
11
12
|
export interface StudioHeaderProps {
|
|
12
13
|
captureFrameHref: string;
|
|
@@ -44,6 +45,7 @@ export function StudioHeader({
|
|
|
44
45
|
inspectorPanelActive,
|
|
45
46
|
onExport,
|
|
46
47
|
}: StudioHeaderProps) {
|
|
48
|
+
const showThemeToggle = useContext(ShowThemeToggle);
|
|
47
49
|
const { projectId, renderQueue } = useStudioShellContext();
|
|
48
50
|
const { rightCollapsed, setRightCollapsed, setRightPanelTab } = usePanelLayoutContext();
|
|
49
51
|
const isRendering = renderQueue.isRendering;
|
|
@@ -147,6 +149,7 @@ export function StudioHeader({
|
|
|
147
149
|
</Button>
|
|
148
150
|
</Tooltip>
|
|
149
151
|
</div>
|
|
152
|
+
{showThemeToggle && <ThemeToggle />}
|
|
150
153
|
<Dock.WindowMenu />
|
|
151
154
|
<Tooltip
|
|
152
155
|
label={
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { setStudioTheme, useShownStudioTheme } from "../utils/studioTheme";
|
|
3
|
+
import { IconButton, Tooltip } from "./ui";
|
|
4
|
+
|
|
5
|
+
// hyperframes.dev's theme toggle: the same sun and moon, revealing the new theme in a circle from
|
|
6
|
+
// the button. A page that paints no frame aborts the transition; the theme is applied by then.
|
|
7
|
+
let latest: ViewTransition | null = null;
|
|
8
|
+
|
|
9
|
+
function reveal(transition: ViewTransition, at: string, radiusPct: number) {
|
|
10
|
+
latest = transition;
|
|
11
|
+
const done = () => {
|
|
12
|
+
if (latest === transition) delete document.documentElement.dataset.themeReveal;
|
|
13
|
+
};
|
|
14
|
+
void transition.finished.then(done, done);
|
|
15
|
+
void transition.ready.then(
|
|
16
|
+
() => {
|
|
17
|
+
document.documentElement.animate(
|
|
18
|
+
{ clipPath: [`circle(0% ${at})`, `circle(${radiusPct}% ${at})`] },
|
|
19
|
+
{ duration: 500, easing: "ease-in-out", pseudoElement: "::view-transition-new(root)" },
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
() => {},
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function origin(e: React.MouseEvent<HTMLButtonElement>): [number, number] {
|
|
27
|
+
if (e.detail > 0) return [e.clientX, e.clientY];
|
|
28
|
+
const box = e.currentTarget.getBoundingClientRect();
|
|
29
|
+
return [box.left + box.width / 2, box.top + box.height / 2];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** On only in Studio's own app; a host that embeds Studio owns its theme. */
|
|
33
|
+
export const ShowThemeToggle = createContext(false);
|
|
34
|
+
|
|
35
|
+
function cannotReveal(): boolean {
|
|
36
|
+
return (
|
|
37
|
+
!document.startViewTransition ||
|
|
38
|
+
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const iconProps = {
|
|
43
|
+
width: "14",
|
|
44
|
+
height: "14",
|
|
45
|
+
viewBox: "0 0 24 24",
|
|
46
|
+
fill: "none",
|
|
47
|
+
stroke: "currentColor",
|
|
48
|
+
strokeWidth: "2",
|
|
49
|
+
strokeLinecap: "round",
|
|
50
|
+
strokeLinejoin: "round",
|
|
51
|
+
"aria-hidden": true,
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
export function ThemeToggle() {
|
|
55
|
+
const isDark = useShownStudioTheme() === "dark";
|
|
56
|
+
const label = `Switch to ${isDark ? "light" : "dark"} theme`;
|
|
57
|
+
|
|
58
|
+
function toggle(e: React.MouseEvent<HTMLButtonElement>) {
|
|
59
|
+
const apply = () => setStudioTheme(isDark ? "light" : "dark");
|
|
60
|
+
if (cannotReveal()) return apply();
|
|
61
|
+
const [x, y] = origin(e);
|
|
62
|
+
const w = window.innerWidth;
|
|
63
|
+
const h = window.innerHeight;
|
|
64
|
+
const maxRadius = Math.hypot(Math.max(x, w - x), Math.max(y, h - y));
|
|
65
|
+
const at = `at ${(x / w) * 100}% ${(y / h) * 100}%`;
|
|
66
|
+
const radiusPct = (maxRadius / (Math.hypot(w, h) / Math.SQRT2)) * 100;
|
|
67
|
+
// The switch stays synchronous inside the callback: rendering is paused during it.
|
|
68
|
+
document.documentElement.dataset.themeReveal = "";
|
|
69
|
+
reveal(document.startViewTransition(apply), at, radiusPct);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Tooltip label={label} side="bottom">
|
|
74
|
+
<IconButton
|
|
75
|
+
onClick={toggle}
|
|
76
|
+
aria-label={label}
|
|
77
|
+
icon={
|
|
78
|
+
isDark ? (
|
|
79
|
+
<svg {...iconProps}>
|
|
80
|
+
<circle cx="12" cy="12" r="5" />
|
|
81
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
82
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
83
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
84
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
85
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
86
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
87
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
88
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
89
|
+
</svg>
|
|
90
|
+
) : (
|
|
91
|
+
<svg {...iconProps}>
|
|
92
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
93
|
+
</svg>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -31,6 +31,8 @@ import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
|
|
|
31
31
|
import { CanvasContextMenu } from "./CanvasContextMenu";
|
|
32
32
|
import { useInlineTextEditing } from "./useInlineTextEditing";
|
|
33
33
|
import { usePreviewReadOnly } from "./previewReadOnlyContext";
|
|
34
|
+
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
35
|
+
import { noteStudioManualEditSave as noteSave } from "./manualEditsDom";
|
|
34
36
|
import type { ZOrderAction, ZOrderPatch } from "./canvasContextMenuZOrder";
|
|
35
37
|
import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
|
|
36
38
|
import { logSelect } from "../../utils/selectDebug";
|
|
@@ -193,16 +195,34 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
193
195
|
onTextEditingChangeRef.current?.(true);
|
|
194
196
|
return () => onTextEditingChangeRef.current?.(false);
|
|
195
197
|
}, [inlineText.editing]);
|
|
198
|
+
// Each canvas save is counted before it starts, so a reload requested earlier loads again.
|
|
196
199
|
const onPathOffsetCommitRef = useRef(onPathOffsetCommit);
|
|
197
|
-
onPathOffsetCommitRef.current =
|
|
200
|
+
onPathOffsetCommitRef.current = (sel, ...rest) => {
|
|
201
|
+
noteSave(sel.element);
|
|
202
|
+
return onPathOffsetCommit(sel, ...rest);
|
|
203
|
+
};
|
|
198
204
|
const onGroupPathOffsetCommitRef = useRef(onGroupPathOffsetCommit);
|
|
199
|
-
onGroupPathOffsetCommitRef.current =
|
|
205
|
+
onGroupPathOffsetCommitRef.current = (updates) => {
|
|
206
|
+
if (updates[0]) noteSave(updates[0].selection.element);
|
|
207
|
+
return onGroupPathOffsetCommit(updates);
|
|
208
|
+
};
|
|
200
209
|
const onBoxSizeCommitRef = useRef(onBoxSizeCommit);
|
|
201
|
-
onBoxSizeCommitRef.current =
|
|
210
|
+
onBoxSizeCommitRef.current = (sel, ...rest) => {
|
|
211
|
+
noteSave(sel.element);
|
|
212
|
+
return onBoxSizeCommit(sel, ...rest);
|
|
213
|
+
};
|
|
202
214
|
const onRotationCommitRef = useRef(onRotationCommit);
|
|
203
|
-
onRotationCommitRef.current =
|
|
215
|
+
onRotationCommitRef.current = (sel, next) => {
|
|
216
|
+
noteSave(sel.element);
|
|
217
|
+
return onRotationCommit(sel, next);
|
|
218
|
+
};
|
|
204
219
|
const onStyleCommitRef = useRef(onStyleCommit);
|
|
205
|
-
onStyleCommitRef.current =
|
|
220
|
+
onStyleCommitRef.current =
|
|
221
|
+
onStyleCommit &&
|
|
222
|
+
((property, value) => {
|
|
223
|
+
if (selectionRef.current) noteSave(selectionRef.current.element);
|
|
224
|
+
return onStyleCommit(property, value);
|
|
225
|
+
});
|
|
206
226
|
const onBlockedMoveRef = useRef(onBlockedMove);
|
|
207
227
|
onBlockedMoveRef.current = onBlockedMove;
|
|
208
228
|
const onManualDragStartRef = useRef(onManualDragStart);
|
|
@@ -282,6 +302,17 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
282
302
|
useEffect(() => {
|
|
283
303
|
if (readOnly) gestures.clearPointerState(selectionRef);
|
|
284
304
|
}, [gestures, readOnly, selectionRef]);
|
|
305
|
+
// A gesture that loses its pointer, the window or the overlay is cancelled, so its mark goes too.
|
|
306
|
+
const cancelGestureRef = useRef(() => {});
|
|
307
|
+
cancelGestureRef.current = () => gestures.clearPointerState(selectionRef);
|
|
308
|
+
useMountEffect(() => {
|
|
309
|
+
const cancel = () => cancelGestureRef.current();
|
|
310
|
+
window.addEventListener("blur", cancel);
|
|
311
|
+
return () => {
|
|
312
|
+
window.removeEventListener("blur", cancel);
|
|
313
|
+
cancel();
|
|
314
|
+
};
|
|
315
|
+
});
|
|
285
316
|
|
|
286
317
|
// Arrow-key nudge (1px, Shift = 10px) — commits through the same
|
|
287
318
|
// path-offset callbacks as a drag, one undo entry per key burst.
|
|
@@ -463,6 +494,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
463
494
|
onPointerLeave={() => onCanvasPointerLeaveRef.current()}
|
|
464
495
|
onPointerUp={marquee.onPointerUp}
|
|
465
496
|
onPointerCancel={marquee.onPointerCancel}
|
|
497
|
+
onLostPointerCapture={() => cancelGestureRef.current()}
|
|
466
498
|
onContextMenu={hostInput ? undefined : handleContextMenu}
|
|
467
499
|
>
|
|
468
500
|
{!hostInput && hoverSelection && hoverRect && compRect.width > 0 && (
|
|
@@ -11,12 +11,12 @@ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
|
11
11
|
import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language";
|
|
12
12
|
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
13
13
|
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
|
14
|
-
import { oneDark } from "@codemirror/theme-one-dark";
|
|
15
14
|
import type { Extension } from "@codemirror/state";
|
|
16
15
|
import { html } from "@codemirror/lang-html";
|
|
17
16
|
import { css } from "@codemirror/lang-css";
|
|
18
17
|
import { javascript } from "@codemirror/lang-javascript";
|
|
19
18
|
import { markdown } from "@codemirror/lang-markdown";
|
|
19
|
+
import { studioCodeTheme } from "./studioCodeTheme";
|
|
20
20
|
|
|
21
21
|
// Marks a programmatic doc sync (external content push — e.g. a manual-edit
|
|
22
22
|
// commit writing the source) so the update listener doesn't mistake it for a
|
|
@@ -114,13 +114,9 @@ export const SourceEditor = memo(function SourceEditor({
|
|
|
114
114
|
highlightSelectionMatches(),
|
|
115
115
|
keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap]),
|
|
116
116
|
getLanguageExtension(lang),
|
|
117
|
-
|
|
117
|
+
studioCodeTheme,
|
|
118
118
|
updateListener,
|
|
119
119
|
EditorState.readOnly.of(readOnly),
|
|
120
|
-
EditorView.theme({
|
|
121
|
-
"&": { height: "100%" },
|
|
122
|
-
".cm-scroller": { overflow: "auto" },
|
|
123
|
-
}),
|
|
124
120
|
],
|
|
125
121
|
});
|
|
126
122
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { readCssRotationTarget, readRotationBase } from "./rotationDraft";
|
|
13
13
|
import {
|
|
14
14
|
beginStudioManualEditGesture,
|
|
15
|
+
endStudioManualEditGesture,
|
|
15
16
|
captureStudioBoxSize,
|
|
16
17
|
captureStudioPathOffset,
|
|
17
18
|
captureStudioRotation,
|
|
@@ -81,48 +82,54 @@ export function startGroupDrag(
|
|
|
81
82
|
}
|
|
82
83
|
members.push(result.member);
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
members.
|
|
90
|
-
member
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
try {
|
|
86
|
+
resetDragMoveLog();
|
|
87
|
+
logDrag("group-start", {
|
|
88
|
+
// A member whose mapping differs from its neighbours travels a different
|
|
89
|
+
// distance for the same pointer delta, which is the group coming apart.
|
|
90
|
+
members: Object.fromEntries(
|
|
91
|
+
members.map((member) => [
|
|
92
|
+
member.key,
|
|
93
|
+
{
|
|
94
|
+
map: `${member.screenToOffset.a.toFixed(3)},${member.screenToOffset.d.toFixed(3)}`,
|
|
95
|
+
base: `${Math.round(member.baseGsap.x)},${Math.round(member.baseGsap.y)}`,
|
|
96
|
+
offset: `${Math.round(member.initialOffset.x)},${Math.round(member.initialOffset.y)}`,
|
|
97
|
+
},
|
|
98
|
+
]),
|
|
99
|
+
),
|
|
100
|
+
at: readDragPositions(members),
|
|
101
|
+
});
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
iframe,
|
|
108
|
-
excludeElements: buildExcludeElements({
|
|
103
|
+
const overlayEl = opts.overlayRef.current;
|
|
104
|
+
const iframe = opts.iframeRef.current;
|
|
105
|
+
const snapContext =
|
|
106
|
+
overlayEl && iframe
|
|
107
|
+
? collectSnapContext({
|
|
108
|
+
overlayEl,
|
|
109
109
|
iframe,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
excludeElements: buildExcludeElements({
|
|
111
|
+
iframe,
|
|
112
|
+
groupSelections: items.map((i) => i.selection),
|
|
113
|
+
}),
|
|
114
|
+
})
|
|
115
|
+
: undefined;
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
120
|
+
opts.rafPausedRef.current = true;
|
|
121
|
+
opts.groupGestureRef.current = {
|
|
122
|
+
startX: e.clientX,
|
|
123
|
+
startY: e.clientY,
|
|
124
|
+
originItems: items,
|
|
125
|
+
members,
|
|
126
|
+
snapContext,
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
opts.groupGestureRef.current = null;
|
|
130
|
+
restoreManualOffsetDragMembers(members);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
126
133
|
return true;
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -219,84 +226,89 @@ export function startGesture(
|
|
|
219
226
|
}
|
|
220
227
|
}
|
|
221
228
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
// A throw before the gesture is armed would leave its mark, and the mark holds every reload.
|
|
230
|
+
try {
|
|
231
|
+
// Rotation base: the angle the element shows. An element GSAP does not turn, or a plain-translate
|
|
232
|
+
// move, never asks GSAP: reading a property makes it bake the CSS into its transform.
|
|
233
|
+
const plain = !!pathOffsetMember?.plainTranslate || !gsapWritesRotation(sel.element);
|
|
234
|
+
const plainRotation = plain && kind === "rotate" ? readCssRotationTarget(sel.element) : null;
|
|
235
|
+
const rotation = { angle: readRotationBase(sel.element, plain) };
|
|
236
|
+
const overlayBounds = overlayEl?.getBoundingClientRect();
|
|
237
|
+
const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
|
|
238
|
+
const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
|
|
230
239
|
|
|
231
|
-
|
|
240
|
+
const iframe = opts.iframeRef.current;
|
|
232
241
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
242
|
+
// A corner resize pins the centroid of the four rotation-aware corners: an axis-aligned
|
|
243
|
+
// size delta only holds the centre still for a symmetric, unrotated growth.
|
|
244
|
+
let resizeFixedCenterStart: { x: number; y: number } | undefined;
|
|
245
|
+
if (kind === "resize" && pathOffsetMember && overlayEl && iframe) {
|
|
246
|
+
const corners = elementCornerOverlayPoints(overlayEl, iframe, sel.element);
|
|
247
|
+
if (corners) resizeFixedCenterStart = overlayCornersCentroid(corners);
|
|
248
|
+
}
|
|
249
|
+
const snapContext =
|
|
250
|
+
(kind === "drag" || kind === "resize") && overlayEl && iframe
|
|
251
|
+
? collectSnapContext({
|
|
252
|
+
overlayEl,
|
|
253
|
+
iframe,
|
|
254
|
+
excludeElements: buildExcludeElements({ iframe, selection: sel }),
|
|
255
|
+
})
|
|
256
|
+
: undefined;
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
e.stopPropagation();
|
|
259
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
260
|
+
opts.rafPausedRef.current = true;
|
|
261
|
+
opts.gestureRef.current = {
|
|
262
|
+
kind,
|
|
263
|
+
mode,
|
|
264
|
+
selection: sel,
|
|
265
|
+
startX: e.clientX,
|
|
266
|
+
startY: e.clientY,
|
|
267
|
+
centerX,
|
|
268
|
+
centerY,
|
|
269
|
+
initialPathOffset,
|
|
270
|
+
initialRotation: captureStudioRotation(sel.element),
|
|
271
|
+
initialBoxSize: captureStudioBoxSize(sel.element),
|
|
272
|
+
pathOffsetMember,
|
|
273
|
+
originLeft: rect.left,
|
|
274
|
+
originTop: rect.top,
|
|
275
|
+
originWidth: rect.width,
|
|
276
|
+
originHeight: rect.height,
|
|
277
|
+
actualWidth,
|
|
278
|
+
actualHeight,
|
|
279
|
+
actualRotation: rotation.angle,
|
|
280
|
+
plainRotation,
|
|
281
|
+
editScaleX: rect.editScaleX,
|
|
282
|
+
editScaleY: rect.editScaleY,
|
|
283
|
+
contentScaleX,
|
|
284
|
+
contentScaleY,
|
|
285
|
+
manualEditDragToken,
|
|
286
|
+
snapContext,
|
|
287
|
+
resizeHandle: kind === "resize" ? (options?.resizeHandle ?? "se") : undefined,
|
|
288
|
+
resizeFixedCenterStart,
|
|
289
|
+
};
|
|
290
|
+
if (kind === "resize") {
|
|
291
|
+
resetResizeMoveLog();
|
|
292
|
+
logResize("start", {
|
|
293
|
+
handle: options?.resizeHandle ?? "se",
|
|
294
|
+
pointer: { x: e.clientX, y: e.clientY },
|
|
295
|
+
center: { x: centerX, y: centerY },
|
|
296
|
+
origin: { left: rect.left, top: rect.top, w: rect.width, h: rect.height },
|
|
297
|
+
actual: { w: actualWidth, h: actualHeight },
|
|
298
|
+
editScale: { x: rect.editScaleX, y: rect.editScaleY },
|
|
299
|
+
contentScale: { x: contentScaleX, y: contentScaleY },
|
|
300
|
+
rotation: rotation.angle,
|
|
301
|
+
hasOffsetMember: !!pathOffsetMember,
|
|
302
|
+
fixedCenterStart: resizeFixedCenterStart ?? null,
|
|
303
|
+
initialBoxSize: opts.gestureRef.current?.initialBoxSize ?? null,
|
|
304
|
+
initialInlineStyle: sel.element.getAttribute("style"),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
opts.gestureRef.current = null;
|
|
309
|
+
if (pathOffsetMember) restoreManualOffsetDragMembers([pathOffsetMember]);
|
|
310
|
+
else if (manualEditDragToken) endStudioManualEditGesture(sel.element, manualEditDragToken);
|
|
311
|
+
throw error;
|
|
300
312
|
}
|
|
301
313
|
return true;
|
|
302
314
|
}
|