@hyperframes/studio 0.8.106 → 0.8.108
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-dl6K6ABp.js → hyperframes-player-BakGew45.js} +1 -1
- package/dist/assets/{index-ooaZZhKf.js → index-C5dyUHwk.js} +1 -1
- package/dist/assets/{index-CMiYVGv5.js → index-C842tcVa.js} +203 -208
- package/dist/assets/{index-DKT5NF1Y.js → index-CPe7d_wO.js} +1 -1
- package/dist/assets/{index-lrT-gfHa.css → index-ncVJ5IVu.css} +1 -1
- package/dist/chunk-6LUNLTHR.js +611 -0
- package/dist/chunk-6LUNLTHR.js.map +1 -0
- package/dist/{chunk-ICTZJPQQ.js → chunk-AOWVEVEU.js} +3 -494
- package/dist/chunk-AOWVEVEU.js.map +1 -0
- package/dist/{chunk-46XP7J3B.js → chunk-PQFEBX32.js} +5 -3
- package/dist/chunk-PQFEBX32.js.map +1 -0
- package/dist/{domEditingLayers-AQBDFNUC.js → domEditingLayers-JQPQGGT7.js} +3 -3
- package/dist/index.d.ts +377 -12
- package/dist/index.html +2 -2
- package/dist/index.js +13445 -11874
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/App.tsx +6 -3
- package/src/agentToolsExport.test.ts +8 -0
- package/src/captions/keyboard.test.ts +6 -0
- package/src/components/EditorShell.tsx +2 -0
- package/src/components/StudioRightPanels.tsx +2 -0
- package/src/components/TimelineToolbar.test.tsx +27 -0
- package/src/components/TimelineToolbar.tsx +2 -5
- package/src/components/editor/CropPresetBar.tsx +69 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +44 -0
- package/src/components/editor/DomEditCropHandles.tsx +123 -95
- package/src/components/editor/DomEditOverlay.tsx +12 -18
- package/src/components/editor/PropertyPanel.tsx +1 -14
- package/src/components/editor/PropertyPanelFlat.tsx +3 -6
- package/src/components/editor/SnapToolbar.test.tsx +32 -1
- package/src/components/editor/SnapToolbar.tsx +3 -4
- package/src/components/editor/carveOutput.ts +54 -0
- package/src/components/editor/cropPresetStore.ts +33 -0
- package/src/components/editor/cropPresets.test.ts +41 -0
- package/src/components/editor/cropPresets.ts +24 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/domEditingLayers.test.ts +21 -0
- package/src/components/editor/domEditingLayers.ts +2 -0
- package/src/components/editor/dragUndoPaint.test.ts +71 -0
- package/src/components/editor/fontAssets.test.ts +12 -22
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +96 -33
- package/src/components/editor/gsapAnimatesProperty.ts +3 -0
- package/src/components/editor/manualEdits.test.ts +4 -3
- package/src/components/editor/manualEditsDom.ts +28 -5
- package/src/components/editor/manualEditsDomPatches.ts +5 -1
- package/src/components/editor/manualOffsetDrag.ts +15 -1
- package/src/components/editor/mediaAudioEdits.test.ts +271 -0
- package/src/components/editor/mediaAudioEdits.ts +282 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +32 -4
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +3 -1
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +290 -87
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +17 -20
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +17 -27
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useDomEditNudge.test.tsx +134 -2
- package/src/components/editor/useDomEditNudge.ts +46 -23
- package/src/components/editor/useDomEditOverlayGestures.ts +17 -6
- package/src/components/editor/useFxCarve.ts +3 -61
- package/src/components/editor/useFxCarveGrouping.test.ts +13 -0
- package/src/components/editor/useFxCarveGrouping.ts +8 -3
- package/src/components/nle/AudioMeterStrip.test.tsx +53 -0
- package/src/components/nle/AudioMeterStrip.tsx +60 -2
- package/src/components/nle/useTimelineEditCallbacks.ts +4 -0
- package/src/components/panels/SlideshowPanel.test.ts +27 -0
- package/src/components/panels/SlideshowPanel.tsx +12 -7
- package/src/contexts/DomEditContext.test.tsx +62 -0
- package/src/contexts/DomEditContext.tsx +94 -77
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/hooks/canvasQuickSaves.test.tsx +72 -7
- package/src/hooks/domEditCommitTypes.ts +10 -0
- package/src/hooks/domEditPersistFailure.ts +12 -1
- package/src/hooks/domStyleCommit.fonts.test.ts +5 -4
- package/src/hooks/domStyleCommit.ts +2 -8
- package/src/hooks/gsapDragCommit.ts +3 -8
- package/src/hooks/gsapDragPositionCommit.ts +6 -1
- package/src/hooks/gsapEditOutcome.ts +2 -1
- package/src/hooks/gsapResizeElementSize.test.tsx +136 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +3 -1
- package/src/hooks/gsapResizeIntercept.test.ts +3 -11
- package/src/hooks/gsapResizeIntercept.ts +6 -2
- package/src/hooks/gsapResizeMixedTween.test.ts +1 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +55 -0
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +1 -3
- package/src/hooks/inspectorSaveReload.test.tsx +107 -0
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +16 -1
- package/src/hooks/useAppHotkeys.test.ts +17 -0
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -1
- package/src/hooks/useAppHotkeys.ts +3 -3
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +17 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +3 -9
- package/src/hooks/useDomEditAttributeCommits.ts +71 -29
- package/src/hooks/useDomEditCommits.test.tsx +172 -92
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditCommitsHelpers.ts +4 -6
- package/src/hooks/useDomEditPersist.ts +28 -13
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -3
- package/src/hooks/useDomEditTextCommits.ts +4 -8
- package/src/hooks/useDomGeometryCommits.ts +11 -2
- package/src/hooks/useEditHistoryActions.paint.test.tsx +163 -3
- package/src/hooks/useEditHistoryActions.ts +13 -4
- package/src/hooks/useElementPicker.test.ts +62 -13
- package/src/hooks/useElementPicker.ts +10 -2
- package/src/hooks/useFreezeFrame.test.ts +56 -0
- package/src/hooks/useFreezeFrame.ts +121 -0
- package/src/hooks/useGsapAwareEditing.ts +6 -6
- package/src/hooks/useGsapScriptCommits.test.tsx +19 -0
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/useInlineTextEdit.test.tsx +27 -0
- package/src/hooks/useInlineTextEdit.ts +9 -7
- package/src/hooks/useKeyframeKeyboard.test.tsx +15 -0
- package/src/hooks/useKeyframeKeyboard.ts +2 -8
- package/src/hooks/usePersistentEditHistory.test.ts +9 -6
- package/src/hooks/usePersistentEditHistory.ts +1 -1
- package/src/hooks/usePreviewPersistence.ts +6 -0
- package/src/hooks/useRazorSplit.ts +14 -1
- package/src/hooks/useRenderClipContent.test.ts +102 -0
- package/src/hooks/useRenderClipContent.ts +30 -23
- package/src/hooks/useTimelineEditing.ts +2 -2
- package/src/hooks/useTrackPendingTimelineEdit.ts +2 -6
- package/src/index.ts +8 -0
- package/src/player/components/AudibleVideoClipContent.tsx +29 -0
- package/src/player/components/ClipBadges.test.tsx +82 -0
- package/src/player/components/ClipBadges.tsx +67 -0
- package/src/player/components/ClipContextMenu.tsx +22 -0
- package/src/player/components/ClipPeakMarks.test.tsx +41 -0
- package/src/player/components/ClipPeakMarks.tsx +71 -0
- package/src/player/components/Timeline.test.ts +0 -21
- package/src/player/components/Timeline.tsx +0 -1
- package/src/player/components/TimelineClip.tsx +2 -0
- package/src/player/components/TimelineFxButton.test.tsx +2 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +37 -0
- package/src/player/components/TimelineTrackHeader.tsx +12 -25
- package/src/player/components/clipMenuAudioItems.test.tsx +125 -0
- package/src/player/components/clipMenuAudioItems.tsx +108 -0
- package/src/player/components/clipMenuDuck.test.ts +117 -0
- package/src/player/components/clipMenuDuck.ts +142 -0
- package/src/player/components/clipMenuNormalize.test.ts +95 -0
- package/src/player/components/clipMenuNormalize.ts +96 -0
- package/src/player/components/clipMenuToolItems.test.tsx +139 -0
- package/src/player/components/clipMenuToolItems.tsx +235 -0
- package/src/player/components/clipPeakRuns.test.ts +38 -0
- package/src/player/components/clipPeakRuns.ts +59 -0
- package/src/player/components/clipToolAttrs.test.ts +161 -0
- package/src/player/components/clipToolAttrs.ts +187 -0
- package/src/player/components/timelineCallbacks.ts +1 -0
- package/src/player/components/timelineLayout.ts +0 -28
- package/src/player/components/useClipToolState.ts +92 -0
- package/src/player/hooks/usePlaybackKeyboard.test.ts +43 -0
- package/src/player/lib/playbackShortcuts.ts +3 -7
- package/src/player/lib/timelineElementHelpers.test.ts +16 -0
- package/src/player/lib/timelineElementHelpers.ts +25 -2
- package/src/player/lib/timelineViewportBudgets.test.ts +2 -0
- package/src/player/lib/timelineViewportBudgets.ts +4 -0
- package/src/timelineChromeExports.test.tsx +7 -0
- package/src/utils/audioMeterMath.test.ts +37 -0
- package/src/utils/audioMeterMath.ts +9 -3
- package/src/utils/exportPeakCeiling.ts +1 -0
- package/src/utils/gsapSoftReload.ts +3 -2
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -0
- package/src/utils/gsapUndoRestore.test.ts +145 -1
- package/src/utils/gsapUndoRestore.ts +70 -21
- package/src/utils/previewRelativePath.ts +19 -0
- package/src/utils/sourcePatcher.ts +2 -2
- package/src/utils/studioFontHelpers.ts +2 -23
- package/src/utils/studioHelpers.test.ts +30 -0
- package/src/utils/studioHelpers.ts +18 -4
- package/src/utils/studioPendingEdits.test.ts +129 -0
- package/src/utils/studioPendingEdits.ts +61 -18
- package/src/utils/timelineInspector.test.ts +33 -2
- package/src/utils/timelineInspector.ts +16 -4
- package/src/utils/typingTarget.test.ts +13 -0
- package/src/utils/typingTarget.ts +5 -2
- package/src/webmcp/useStudioAgentTools.ts +3 -1
- package/dist/chunk-46XP7J3B.js.map +0 -1
- package/dist/chunk-DI4LCJHL.js +0 -102
- package/dist/chunk-DI4LCJHL.js.map +0 -1
- package/dist/chunk-ICTZJPQQ.js.map +0 -1
- /package/dist/{domEditingLayers-AQBDFNUC.js.map → domEditingLayers-JQPQGGT7.js.map} +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act, createElement } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
5
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
6
|
+
import {
|
|
7
|
+
makePreview,
|
|
8
|
+
mountPlayerWithPreview,
|
|
9
|
+
paintShadow,
|
|
10
|
+
resetPlayerStore,
|
|
11
|
+
} from "../player/hooks/timelinePlayerTestHarness";
|
|
12
|
+
import { useDomEditCommits } from "./useDomEditCommits";
|
|
13
|
+
|
|
14
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
15
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
16
|
+
vi.mock("../utils/gsapSoftReload", async (importOriginal) => ({
|
|
17
|
+
...(await importOriginal<typeof import("../utils/gsapSoftReload")>()),
|
|
18
|
+
ensureMotionPathPluginLoaded: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
/** The project file; the patch route applies a colour to it, and each preview load serves it. */
|
|
22
|
+
let color = "red";
|
|
23
|
+
const card = () => `<div data-hf-id="hf-card" style="color: ${color}">Card</div>`;
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.unstubAllGlobals();
|
|
27
|
+
document.body.innerHTML = "";
|
|
28
|
+
resetPlayerStore();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("an inspector save made while a reload loads is not undone when that reload goes on screen", async () => {
|
|
32
|
+
color = "red";
|
|
33
|
+
let land = () => {};
|
|
34
|
+
const landed = new Promise<void>((resolve) => (land = resolve));
|
|
35
|
+
vi.stubGlobal(
|
|
36
|
+
"fetch",
|
|
37
|
+
vi.fn(async (input: string, init?: RequestInit) => {
|
|
38
|
+
if (!input.includes("/file-mutations/patch-element/"))
|
|
39
|
+
return Response.json({ content: card() });
|
|
40
|
+
await landed;
|
|
41
|
+
const { operations } = JSON.parse(String(init?.body)) as { operations: { value: string }[] };
|
|
42
|
+
color = operations[0]!.value;
|
|
43
|
+
return Response.json({ ok: true, changed: true, matched: true, content: card() });
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
const live = makePreview(card());
|
|
47
|
+
const player = mountPlayerWithPreview(live);
|
|
48
|
+
const element = live.contentDocument!.querySelector<HTMLElement>("[data-hf-id]")!;
|
|
49
|
+
const selection = {
|
|
50
|
+
element,
|
|
51
|
+
label: "Card",
|
|
52
|
+
tagName: "div",
|
|
53
|
+
sourceFile: "index.html",
|
|
54
|
+
compositionPath: "index.html",
|
|
55
|
+
textFields: [],
|
|
56
|
+
inlineStyles: { color: "red" },
|
|
57
|
+
computedStyles: {},
|
|
58
|
+
dataAttributes: {},
|
|
59
|
+
capabilities: { canSelect: true, canEditStyles: true },
|
|
60
|
+
hfId: "hf-card",
|
|
61
|
+
selector: '[data-hf-id="hf-card"]',
|
|
62
|
+
selectorIndex: 0,
|
|
63
|
+
} as unknown as DomEditSelection;
|
|
64
|
+
let commits: ReturnType<typeof useDomEditCommits> | null = null;
|
|
65
|
+
function Inspector() {
|
|
66
|
+
commits = useDomEditCommits({
|
|
67
|
+
activeCompPath: "index.html",
|
|
68
|
+
previewIframeRef: player.getApi().iframeRef,
|
|
69
|
+
showToast: () => {},
|
|
70
|
+
queueDomEditSave: (save) => save(),
|
|
71
|
+
writeProjectFile: async () => {},
|
|
72
|
+
editHistory: { recordEdit: async () => {} },
|
|
73
|
+
fileTree: [],
|
|
74
|
+
importedFontAssetsRef: { current: [] },
|
|
75
|
+
projectId: "p1",
|
|
76
|
+
projectIdRef: { current: "p1" },
|
|
77
|
+
reloadPreview: () => {},
|
|
78
|
+
domEditSelection: selection,
|
|
79
|
+
applyDomSelection: () => {},
|
|
80
|
+
clearDomSelection: () => {},
|
|
81
|
+
refreshDomEditSelectionFromPreview: () => {},
|
|
82
|
+
buildDomSelectionFromTarget: async () => null,
|
|
83
|
+
readOnlyPreview: false,
|
|
84
|
+
});
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const root = createRoot(document.createElement("div"));
|
|
88
|
+
act(() => root.render(createElement(Inspector)));
|
|
89
|
+
|
|
90
|
+
act(() => player.getApi().refreshPlayer());
|
|
91
|
+
const beforeSave = makePreview(card(), "?_t=1");
|
|
92
|
+
let saved: Promise<unknown> = Promise.resolve();
|
|
93
|
+
act(() => void (saved = commits!.handleDomStyleCommit("color", "green")));
|
|
94
|
+
await act(async () => {
|
|
95
|
+
land();
|
|
96
|
+
await saved;
|
|
97
|
+
});
|
|
98
|
+
expect(color).toBe("green");
|
|
99
|
+
|
|
100
|
+
const requested = await paintShadow(player.getApi, beforeSave);
|
|
101
|
+
expect(player.getApi().iframeRef.current, "the file before the save").toBe(live);
|
|
102
|
+
const fresh = makePreview(card(), "?_t=2");
|
|
103
|
+
expect(await paintShadow(player.getApi, fresh)).toBeGreaterThan(requested);
|
|
104
|
+
expect(player.getApi().iframeRef.current).toBe(fresh);
|
|
105
|
+
act(() => root.unmount());
|
|
106
|
+
act(() => player.root.unmount());
|
|
107
|
+
});
|
|
@@ -16,6 +16,7 @@ vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() }));
|
|
|
16
16
|
const domDelete = vi.fn(async () => undefined);
|
|
17
17
|
const historyUndo = vi.fn(async () => ({ ok: false }));
|
|
18
18
|
const historyRedo = vi.fn(async () => ({ ok: false }));
|
|
19
|
+
const settle = vi.fn(async () => undefined);
|
|
19
20
|
let root: Root | null = null;
|
|
20
21
|
let sync: ((iframe: HTMLIFrameElement | null) => void) | null = null;
|
|
21
22
|
|
|
@@ -49,7 +50,7 @@ function Harness() {
|
|
|
49
50
|
writeProjectFile: vi.fn(async () => undefined),
|
|
50
51
|
showToast: vi.fn(),
|
|
51
52
|
syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
|
|
52
|
-
|
|
53
|
+
settlePendingEdits: settle,
|
|
53
54
|
handleCopy: vi.fn(() => false),
|
|
54
55
|
handlePaste: vi.fn(() => false),
|
|
55
56
|
handleCut: vi.fn(() => false),
|
|
@@ -69,6 +70,8 @@ afterEach(() => {
|
|
|
69
70
|
domDelete.mockClear();
|
|
70
71
|
historyUndo.mockClear();
|
|
71
72
|
historyRedo.mockClear();
|
|
73
|
+
settle.mockReset();
|
|
74
|
+
settle.mockImplementation(async () => undefined);
|
|
72
75
|
vi.mocked(trackStudioEvent).mockClear();
|
|
73
76
|
});
|
|
74
77
|
|
|
@@ -122,6 +125,18 @@ describe("preview iframe hotkey forwarding", () => {
|
|
|
122
125
|
expect(trackStudioEvent).toHaveBeenCalledWith("keyboard_shortcut", { action: "undo" });
|
|
123
126
|
});
|
|
124
127
|
|
|
128
|
+
it("settles pending edits, failed ones included, before Cmd+Z steps the history", async () => {
|
|
129
|
+
let settled!: () => void;
|
|
130
|
+
settle.mockImplementation(
|
|
131
|
+
() => new Promise<undefined>((resolve) => (settled = () => resolve(undefined))),
|
|
132
|
+
);
|
|
133
|
+
await press(mountWithPreview(), { key: "z", ctrlKey: true });
|
|
134
|
+
expect(settle).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(historyUndo).not.toHaveBeenCalled();
|
|
136
|
+
await act(async () => settled());
|
|
137
|
+
expect(historyUndo).toHaveBeenCalledTimes(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
125
140
|
it.each([
|
|
126
141
|
["Shift+Cmd+Z", { key: "Z", metaKey: true, shiftKey: true }],
|
|
127
142
|
["Ctrl+Y", { key: "y", ctrlKey: true }],
|
|
@@ -61,6 +61,9 @@ afterEach(() => {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
describe("keys on a focused slider", () => {
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
document.body.innerHTML = "";
|
|
66
|
+
});
|
|
64
67
|
const slider = () => {
|
|
65
68
|
const el = document.createElement("div");
|
|
66
69
|
el.setAttribute("role", "slider");
|
|
@@ -85,6 +88,20 @@ describe("keys on a focused slider", () => {
|
|
|
85
88
|
expect(cb.handleTimelineElementDelete).not.toHaveBeenCalled();
|
|
86
89
|
});
|
|
87
90
|
|
|
91
|
+
it.each(["video", "audio"])("leaves Delete to a focused <%s controls>", (tag) => {
|
|
92
|
+
usePlayerStore.setState({
|
|
93
|
+
elements: [bgmElement],
|
|
94
|
+
selectedElementId: "bgm",
|
|
95
|
+
selectedElementIds: new Set(["bgm"]),
|
|
96
|
+
});
|
|
97
|
+
const player = document.body.appendChild(document.createElement(tag));
|
|
98
|
+
player.setAttribute("controls", "");
|
|
99
|
+
const cb = callbacks();
|
|
100
|
+
dispatchPlainKey(from(player, { key: "Delete" }), "delete", cb);
|
|
101
|
+
expect(cb.handleTimelineElementsDelete).not.toHaveBeenCalled();
|
|
102
|
+
expect(cb.handleTimelineElementDelete).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
88
105
|
it("still undoes with Cmd+Z while a slider has focus", () => {
|
|
89
106
|
const cb = callbacks();
|
|
90
107
|
dispatchModifierKey(from(slider(), { key: "z", metaKey: true }), "z", cb);
|
|
@@ -86,7 +86,7 @@ function Harness() {
|
|
|
86
86
|
writeProjectFile: vi.fn(async () => undefined),
|
|
87
87
|
showToast: vi.fn(),
|
|
88
88
|
syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
|
|
89
|
-
|
|
89
|
+
settlePendingEdits: vi.fn(async () => undefined),
|
|
90
90
|
handleCopy: vi.fn(() => false),
|
|
91
91
|
handlePaste: vi.fn(async () => undefined),
|
|
92
92
|
handleCut: vi.fn(async () => false),
|
|
@@ -85,7 +85,7 @@ interface UseAppHotkeysParams {
|
|
|
85
85
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
86
86
|
syncHistoryPreviewAfterApply: UseEditHistoryActionsOptions["syncHistoryPreviewAfterApply"];
|
|
87
87
|
showHistoryRestoreNow?: UseEditHistoryActionsOptions["showHistoryRestoreNow"];
|
|
88
|
-
|
|
88
|
+
settlePendingEdits: () => Promise<void>;
|
|
89
89
|
handleCopy: () => boolean;
|
|
90
90
|
handlePaste: () => Promise<void>;
|
|
91
91
|
handleCut: () => Promise<boolean>;
|
|
@@ -124,7 +124,7 @@ export function useAppHotkeys({
|
|
|
124
124
|
showToast,
|
|
125
125
|
syncHistoryPreviewAfterApply,
|
|
126
126
|
showHistoryRestoreNow,
|
|
127
|
-
|
|
127
|
+
settlePendingEdits,
|
|
128
128
|
handleCopy,
|
|
129
129
|
handlePaste,
|
|
130
130
|
handleCut,
|
|
@@ -151,7 +151,7 @@ export function useAppHotkeys({
|
|
|
151
151
|
showToast,
|
|
152
152
|
syncHistoryPreviewAfterApply,
|
|
153
153
|
showHistoryRestoreNow,
|
|
154
|
-
waitForPendingDomEditSaves,
|
|
154
|
+
waitForPendingDomEditSaves: settlePendingEdits,
|
|
155
155
|
onAfterUndoRedo,
|
|
156
156
|
activeCompPath,
|
|
157
157
|
forceReloadSdkSession,
|
|
@@ -216,6 +216,23 @@ describe("useAutomationSelectionKeyboard", () => {
|
|
|
216
216
|
input.remove();
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
describe("with a focused picker", () => {
|
|
220
|
+
const picker = document.createElement("button");
|
|
221
|
+
picker.setAttribute("role", "combobox");
|
|
222
|
+
afterEach(() => picker.remove());
|
|
223
|
+
|
|
224
|
+
it("is inert while a picker that owns typing has focus", () => {
|
|
225
|
+
usePlayerStore
|
|
226
|
+
.getState()
|
|
227
|
+
.setAutomationSelection(wholeAxis({ elementKey: "bgm", target: "volume", t0: 1, t1: 3 }));
|
|
228
|
+
const { onCommit } = setup({});
|
|
229
|
+
document.body.append(picker);
|
|
230
|
+
picker.focus();
|
|
231
|
+
key("Delete");
|
|
232
|
+
expect(onCommit).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
219
236
|
it("Cmd+C copies the active selection", () => {
|
|
220
237
|
clearAutomationClipboard();
|
|
221
238
|
usePlayerStore.setState({ elements: [bgmElement], selectedElementId: "bgm" });
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type HfAutomationLane,
|
|
32
32
|
} from "@hyperframes/core/audio-automation";
|
|
33
33
|
import { clampNumber } from "../utils/studioHelpers";
|
|
34
|
+
import { isTypingTarget } from "../utils/typingTarget";
|
|
34
35
|
import type { AutomationSelection } from "../player/store/automationSelectionSlice";
|
|
35
36
|
import type {
|
|
36
37
|
AutomationLaneBinding,
|
|
@@ -39,13 +40,6 @@ import type {
|
|
|
39
40
|
|
|
40
41
|
type PlayerState = ReturnType<typeof usePlayerStore.getState>;
|
|
41
42
|
|
|
42
|
-
function isTextInput(el: Element | null): boolean {
|
|
43
|
-
if (!el) return false;
|
|
44
|
-
const tag = el.tagName;
|
|
45
|
-
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
46
|
-
return el instanceof HTMLElement && el.isContentEditable;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
43
|
/**
|
|
50
44
|
* A Cmd/Ctrl+<letter> chord. `e.key` is normalised because CapsLock makes it
|
|
51
45
|
* "V"/"C", and useAppHotkeys already lowercases — a raw `e.key === "v"` test
|
|
@@ -302,7 +296,7 @@ function handleCopy(
|
|
|
302
296
|
* dead key beats today's double write.
|
|
303
297
|
*/
|
|
304
298
|
export function automationOwnsKey(e: KeyboardEvent): boolean {
|
|
305
|
-
if (
|
|
299
|
+
if (isTypingTarget(document.activeElement)) return false;
|
|
306
300
|
const state = usePlayerStore.getState();
|
|
307
301
|
if (isChord(e, "c")) return state.automationSelection !== null;
|
|
308
302
|
if (!isChord(e, "v")) return false;
|
|
@@ -340,7 +334,7 @@ export function useAutomationSelectionKeyboard({
|
|
|
340
334
|
}): void {
|
|
341
335
|
useEffect(() => {
|
|
342
336
|
const handler = (e: KeyboardEvent): void => {
|
|
343
|
-
if (
|
|
337
|
+
if (isTypingTarget(document.activeElement)) return;
|
|
344
338
|
// Somebody upstream already claimed this key. useAppHotkeys is on
|
|
345
339
|
// window/capture so it always runs first, and it deliberately lets a
|
|
346
340
|
// keyframe selection outrank an automation range on Delete — without
|
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
getDomEditTargetKey,
|
|
6
6
|
type DomEditSelection,
|
|
7
7
|
} from "../components/editor/domEditing";
|
|
8
|
-
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
9
|
-
import {
|
|
8
|
+
import type { CommitDomAttributeBatch, PersistDomEditOperations } from "./domEditCommitTypes";
|
|
9
|
+
import {
|
|
10
|
+
DomEditPersistPreparedWriteError,
|
|
11
|
+
reportDomEditPersistFailure,
|
|
12
|
+
} from "./domEditPersistFailure";
|
|
10
13
|
import { bumpDomEditCommitMapVersion, runDomEditCommit } from "./domEditCommitRunner";
|
|
11
14
|
import { syncStoredAutomationFromPreview } from "../player/lib/automationStoreSync";
|
|
12
15
|
import { HF_AUDIO_GROUP_ATTR, HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
|
|
@@ -54,6 +57,15 @@ export function resolveOptimisticAttributeValue(attr: string, value: string | nu
|
|
|
54
57
|
return value === "false" && HTML_BOOLEAN_ATTRIBUTES.has(attr) ? null : value;
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
function previewAttributeName(op: PatchOperation): string {
|
|
61
|
+
return op.type === "attribute" ? resolveFullAttrName(op.property, true) : op.property;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function previewAttributeValue(op: PatchOperation): string | null {
|
|
65
|
+
if (op.type === "html-attribute") return resolveOptimisticAttributeValue(op.property, op.value);
|
|
66
|
+
return op.value === "" ? null : op.value;
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
function setOrRemovePreviewAttribute(
|
|
58
70
|
el: HTMLElement,
|
|
59
71
|
fullAttr: string,
|
|
@@ -209,7 +221,7 @@ export function useDomEditAttributeCommits({
|
|
|
209
221
|
],
|
|
210
222
|
);
|
|
211
223
|
|
|
212
|
-
// Commits several data-* attributes on the SAME element in ONE persist call
|
|
224
|
+
// Commits several data-* and HTML attributes on the SAME element in ONE persist call
|
|
213
225
|
// — needed when two attributes together describe a single logical value
|
|
214
226
|
// (e.g. a pinned timing range's start+duration): committing them through two
|
|
215
227
|
// separate sequential `commitDataAttribute` calls leaves a window where the
|
|
@@ -221,32 +233,30 @@ export function useDomEditAttributeCommits({
|
|
|
221
233
|
// explicit, caller-supplied `selection` (not the "current" one) closes both
|
|
222
234
|
// gaps — matching `onCommitAnimatedProperties`'s same-shaped fix for GSAP
|
|
223
235
|
// property batches.
|
|
224
|
-
const
|
|
236
|
+
const commitAttributeOps = useCallback(
|
|
225
237
|
async (
|
|
226
238
|
selection: DomEditSelection,
|
|
227
|
-
|
|
228
|
-
options: DataAttributeCommitOptions
|
|
229
|
-
|
|
239
|
+
ops: PatchOperation[],
|
|
240
|
+
options: DataAttributeCommitOptions & {
|
|
241
|
+
prepareContent?: (html: string) => string;
|
|
242
|
+
syncAutomation?: boolean;
|
|
243
|
+
},
|
|
244
|
+
): Promise<boolean> => {
|
|
230
245
|
const iframe = previewIframeRef.current;
|
|
231
|
-
const entries =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
value,
|
|
246
|
+
const entries = ops.map((op) => ({
|
|
247
|
+
fullAttr: previewAttributeName(op),
|
|
248
|
+
value: previewAttributeValue(op),
|
|
235
249
|
}));
|
|
236
|
-
const commitKey = `${options.coalescePrefix}:${
|
|
237
|
-
.map((
|
|
250
|
+
const commitKey = `${options.coalescePrefix}:${ops
|
|
251
|
+
.map((op) => op.property)
|
|
238
252
|
.sort()
|
|
239
253
|
.join(",")}:${getDomEditTargetKey(selection)}`;
|
|
240
254
|
const isLatestCommit = bumpDomEditCommitMapVersion(
|
|
241
255
|
domAttributeCommitVersionRef.current,
|
|
242
256
|
commitKey,
|
|
243
257
|
);
|
|
244
|
-
const ops: PatchOperation[] = entries.map((entry) => ({
|
|
245
|
-
type: "attribute",
|
|
246
|
-
property: entry.attr,
|
|
247
|
-
value: entry.value,
|
|
248
|
-
}));
|
|
249
258
|
let captured: CapturedMultiAttributeElement | null = null;
|
|
259
|
+
let landed = false;
|
|
250
260
|
|
|
251
261
|
await runDomEditCommit({
|
|
252
262
|
capture: () => {
|
|
@@ -260,8 +270,7 @@ export function useDomEditAttributeCommits({
|
|
|
260
270
|
apply: () => {
|
|
261
271
|
if (!captured) return;
|
|
262
272
|
for (const entry of entries) {
|
|
263
|
-
|
|
264
|
-
setOrRemovePreviewAttribute(captured.element, entry.fullAttr, nextValue);
|
|
273
|
+
setOrRemovePreviewAttribute(captured.element, entry.fullAttr, entry.value);
|
|
265
274
|
}
|
|
266
275
|
},
|
|
267
276
|
persist: () =>
|
|
@@ -269,8 +278,10 @@ export function useDomEditAttributeCommits({
|
|
|
269
278
|
label: options.label,
|
|
270
279
|
coalesceKey: commitKey,
|
|
271
280
|
skipRefresh: options.skipRefresh,
|
|
281
|
+
prepareContent: options.prepareContent,
|
|
272
282
|
}),
|
|
273
|
-
shouldRevert: () =>
|
|
283
|
+
shouldRevert: (error) =>
|
|
284
|
+
isLatestCommit() && !(error instanceof DomEditPersistPreparedWriteError),
|
|
274
285
|
revert: () => {
|
|
275
286
|
if (!captured) return;
|
|
276
287
|
for (const entry of entries) {
|
|
@@ -283,10 +294,19 @@ export function useDomEditAttributeCommits({
|
|
|
283
294
|
},
|
|
284
295
|
onError: (error) => reportDomEditPersistFailure(selection, ops, error, showToast),
|
|
285
296
|
shouldResync: () => isLatestCommit() && !!options.refreshAfter,
|
|
286
|
-
resync: () =>
|
|
287
|
-
|
|
297
|
+
resync: () => {
|
|
298
|
+
refreshDomEditSelectionFromPreview(selection);
|
|
299
|
+
if (options.syncAutomation) {
|
|
300
|
+
syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
onSettled: (ok) => {
|
|
304
|
+
landed = ok;
|
|
305
|
+
options.onSettled?.(ok);
|
|
306
|
+
},
|
|
288
307
|
onFinally: isLatestCommit.release,
|
|
289
308
|
});
|
|
309
|
+
return landed;
|
|
290
310
|
},
|
|
291
311
|
[
|
|
292
312
|
activeCompPath,
|
|
@@ -299,14 +319,35 @@ export function useDomEditAttributeCommits({
|
|
|
299
319
|
|
|
300
320
|
const handleDomAttributesCommit = useCallback(
|
|
301
321
|
async (selection: DomEditSelection, attrs: Record<string, string>) => {
|
|
302
|
-
await
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
await commitAttributeOps(
|
|
323
|
+
selection,
|
|
324
|
+
Object.entries(attrs).map(([property, value]) => ({
|
|
325
|
+
type: "attribute",
|
|
326
|
+
property,
|
|
327
|
+
value,
|
|
328
|
+
})),
|
|
329
|
+
{
|
|
330
|
+
label: "Edit timing",
|
|
331
|
+
coalescePrefix: "attrs",
|
|
332
|
+
skipRefresh: false,
|
|
333
|
+
refreshAfter: true,
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
},
|
|
337
|
+
[commitAttributeOps],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const handleDomAttributeBatchCommit: CommitDomAttributeBatch = useCallback(
|
|
341
|
+
(selection, ops, options) =>
|
|
342
|
+
commitAttributeOps(selection, ops, {
|
|
343
|
+
label: options.label,
|
|
344
|
+
coalescePrefix: "attr-batch",
|
|
305
345
|
skipRefresh: false,
|
|
306
346
|
refreshAfter: true,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
347
|
+
syncAutomation: true,
|
|
348
|
+
prepareContent: options.prepareContent,
|
|
349
|
+
}),
|
|
350
|
+
[commitAttributeOps],
|
|
310
351
|
);
|
|
311
352
|
|
|
312
353
|
const handleDomAttributeCommit = useCallback(
|
|
@@ -438,5 +479,6 @@ export function useDomEditAttributeCommits({
|
|
|
438
479
|
handleDomAttributeQuietCommit,
|
|
439
480
|
handleDomHtmlAttributeCommit,
|
|
440
481
|
handleDomAttributesCommit,
|
|
482
|
+
handleDomAttributeBatchCommit,
|
|
441
483
|
};
|
|
442
484
|
}
|