@hyperframes/studio 0.8.109 → 0.8.111
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-CJYtikAy.js → hyperframes-player-DvJeHBHi.js} +1 -1
- package/dist/assets/{index-CMSgrT2F.js → index--G6c4Q_x.js} +1 -1
- package/dist/assets/{index-HTFVHUJ4.js → index-Bt4vDkfw.js} +1 -1
- package/dist/assets/{index-BQZBDqMs.css → index-C7apTvpr.css} +1 -1
- package/dist/assets/{index-CYP6dMd7.js → index-SFktf7O1.js} +205 -204
- package/dist/index.d.ts +22 -6
- package/dist/index.html +2 -2
- package/dist/index.js +5352 -4755
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/editor/DomEditCropHandles.test.tsx +51 -22
- package/src/components/editor/DomEditCropHandles.tsx +2 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +1 -0
- package/src/components/editor/commitPreflightHandles.test.tsx +4 -1
- package/src/components/editor/domEditOverlayGestures.ts +10 -0
- package/src/components/editor/domEditOverlayStartGesture.test.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +2 -0
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +39 -2
- package/src/components/editor/marqueeCommit.test.tsx +31 -6
- package/src/components/editor/marqueeCommit.ts +16 -8
- package/src/components/editor/mediaLinkEdits.test.ts +19 -6
- package/src/components/editor/mediaLinkEdits.ts +34 -13
- package/src/components/editor/previewReadOnly.overlay.test.tsx +8 -1
- package/src/components/editor/shiftDragAxisLock.test.tsx +17 -3
- package/src/components/editor/useDomEditOverlayGestures.ts +3 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +2 -0
- package/src/contexts/TimelineEditContext.tsx +1 -0
- package/src/editorGesturesExports.test.tsx +1 -0
- package/src/hooks/appHotkeysDispatch.ts +9 -0
- package/src/hooks/timelineElementsAttribute.test.ts +95 -0
- package/src/hooks/timelineElementsAttribute.ts +113 -0
- package/src/hooks/timelineLinkEditPlan.test.ts +28 -0
- package/src/hooks/timelineLinkEditPlan.ts +6 -1
- package/src/hooks/useAppHotkeys.test.ts +34 -0
- package/src/hooks/useDomGeometryCommits.test.tsx +8 -1
- package/src/hooks/useRenderClipContent.ts +3 -5
- package/src/hooks/useTimelineEditing.ts +13 -0
- package/src/hooks/useTimelineLinkEditing.test.tsx +12 -0
- package/src/hooks/useTimelineLinkEditing.ts +5 -0
- package/src/index.ts +5 -0
- package/src/player/components/AudioGainDialog.test.tsx +133 -0
- package/src/player/components/AudioGainDialog.tsx +226 -0
- package/src/player/components/ClipBadges.test.tsx +32 -8
- package/src/player/components/ClipBadges.tsx +37 -31
- package/src/player/components/ClipContextMenu.groups.test.tsx +1 -1
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/ClipPeakMarks.tsx +2 -14
- package/src/player/components/Timeline.test.ts +15 -0
- package/src/player/components/TimelineClip.test.tsx +41 -0
- package/src/player/components/TimelineClip.tsx +3 -2
- package/src/player/components/TimelineLanes.test.tsx +45 -2
- package/src/player/components/TimelineLanes.tsx +0 -2
- package/src/player/components/TimelineOverlays.tsx +2 -0
- package/src/player/components/audioClipLink.test.ts +60 -0
- package/src/player/components/audioClipLink.ts +77 -0
- package/src/player/components/audioGainApply.test.ts +87 -0
- package/src/player/components/audioGainApply.ts +57 -0
- package/src/player/components/audioGainDialogStore.ts +36 -0
- package/src/player/components/audioGainPlan.test.ts +82 -0
- package/src/player/components/audioGainPlan.ts +53 -0
- package/src/player/components/clipMenuAudioItems.noShell.test.tsx +16 -8
- package/src/player/components/clipMenuAudioItems.test.tsx +8 -42
- package/src/player/components/clipMenuAudioItems.tsx +16 -37
- package/src/player/components/clipMenuLinkItems.test.ts +84 -0
- package/src/player/components/clipMenuLinkItems.tsx +54 -9
- package/src/player/components/clipMenuToolItems.test.tsx +130 -4
- package/src/player/components/clipMenuToolItems.tsx +79 -6
- package/src/player/components/clipPeakMap.ts +46 -0
- package/src/player/components/clipPeakRuns.test.ts +14 -1
- package/src/player/components/clipPeakRuns.ts +11 -0
- package/src/player/components/clipToolAttrs.test.ts +57 -41
- package/src/player/components/clipToolAttrs.ts +48 -45
- package/src/player/components/studioShortcuts.ts +2 -0
- package/src/player/components/timelineCallbacks.ts +6 -0
- package/src/player/components/timelineClipDragPreview.test.ts +96 -0
- package/src/player/components/timelineClipDragPreview.ts +32 -7
- package/src/player/components/timelineClipGestureHandlers.ts +26 -31
- package/src/player/components/timelineEditing.test.ts +0 -13
- package/src/player/components/timelineEditing.ts +0 -11
- package/src/player/components/timelineLaneProps.ts +0 -5
- package/src/player/components/timelineLinkSelection.ts +20 -2
- package/src/player/components/timelineViewportGeometry.ts +3 -0
- package/src/player/components/useClipToolState.ts +0 -5
- package/src/player/components/useTimelineClipDrag.ts +12 -5
- package/src/player/components/useTimelineOverlaysState.ts +0 -3
- package/src/player/components/useTimelineProviderState.tsx +0 -2
- package/src/player/components/useTimelineRangeSelection.ts +0 -14
- package/src/player/components/useTimelineRowVirtualization.ts +2 -1
- package/src/utils/sdkCutover.linkedTiming.test.ts +52 -0
- package/src/utils/studioPendingEdits.test.ts +18 -0
- package/src/utils/typingTarget.test.ts +8 -0
- package/src/utils/typingTarget.ts +1 -0
|
@@ -382,17 +382,6 @@ export function resolveBlockedTimelineEditIntent(input: {
|
|
|
382
382
|
return "move";
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
export function buildClipRangeSelection(
|
|
386
|
-
clip: { start: number; duration: number },
|
|
387
|
-
anchor: { anchorX: number; anchorY: number },
|
|
388
|
-
): TimelineRangeSelection {
|
|
389
|
-
return {
|
|
390
|
-
start: clip.start,
|
|
391
|
-
end: clip.start + clip.duration,
|
|
392
|
-
anchorX: anchor.anchorX,
|
|
393
|
-
anchorY: anchor.anchorY,
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
385
|
export function buildTimelineAgentPrompt({
|
|
397
386
|
rangeStart,
|
|
398
387
|
rangeEnd,
|
|
@@ -66,11 +66,6 @@ export interface TimelineLaneBaseProps {
|
|
|
66
66
|
setResizingClip: (v: ResizingClipState | null) => void;
|
|
67
67
|
setDraggedClip: (v: DraggedClipState | null) => void;
|
|
68
68
|
setSelectedElementId: (id: string | null) => void;
|
|
69
|
-
shiftClickClipRef: React.RefObject<{
|
|
70
|
-
element: TimelineElement;
|
|
71
|
-
anchorX: number;
|
|
72
|
-
anchorY: number;
|
|
73
|
-
} | null>;
|
|
74
69
|
getPreviewElement: (element: TimelineElement) => TimelineElement;
|
|
75
70
|
getTrackStyle: (tag: string) => TrackVisualStyle;
|
|
76
71
|
keyframeCache?: Map<string, KeyframeCacheEntry>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { usePlayerStore } from "../store/playerStore";
|
|
2
|
-
import { expandToLinkedMembers } from "./audioClipLink";
|
|
1
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
2
|
+
import { expandToLinkedMembers, linkedMembersOf } from "./audioClipLink";
|
|
3
3
|
import { isLinkedSelectionOn } from "../../utils/linkedClipPreferences";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -21,3 +21,21 @@ export function selectClipWithLinks(
|
|
|
21
21
|
const expanded = expandToLinkedMembers(selectedElementIds, elements);
|
|
22
22
|
if (expanded.size > selectedElementIds.size) state.setSelection(expanded, key);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
export function toggleClipWithLinks(key: string, altKey: boolean): TimelineElement | null {
|
|
26
|
+
const state = usePlayerStore.getState();
|
|
27
|
+
const element = state.elements.find((el) => (el.key ?? el.id) === key);
|
|
28
|
+
const members = element
|
|
29
|
+
? linkedMembersOf(element, state.elements, !altKey && isLinkedSelectionOn())
|
|
30
|
+
: [];
|
|
31
|
+
const memberKeys = members.length > 0 ? members.map((el) => el.key ?? el.id) : [key];
|
|
32
|
+
const next = new Set(state.selectedElementIds);
|
|
33
|
+
const removing = next.has(key);
|
|
34
|
+
for (const memberKey of memberKeys) {
|
|
35
|
+
if (removing) next.delete(memberKey);
|
|
36
|
+
else next.add(memberKey);
|
|
37
|
+
}
|
|
38
|
+
state.setSelection(next, removing ? state.selectedElementId : key);
|
|
39
|
+
const primary = usePlayerStore.getState().selectedElementId;
|
|
40
|
+
return state.elements.find((el) => (el.key ?? el.id) === primary) ?? null;
|
|
41
|
+
}
|
|
@@ -40,7 +40,10 @@ export function getTimelineScrollTopForGeometryChange(
|
|
|
40
40
|
previous: TimelineRowGeometry,
|
|
41
41
|
next: TimelineRowGeometry,
|
|
42
42
|
scrollTop: number,
|
|
43
|
+
dragAutoScrollOwnsScrollTop = false,
|
|
43
44
|
): number {
|
|
45
|
+
const atTopSoRowsAddedAboveStayShown = scrollTop <= 0;
|
|
46
|
+
if (dragAutoScrollOwnsScrollTop || atTopSoRowsAddedAboveStayShown) return scrollTop;
|
|
44
47
|
const anchor = previous.getRowPositionFromY(scrollTop + RULER_H);
|
|
45
48
|
if (anchor.row < 0 || anchor.row >= previous.rowKeys.length) return scrollTop;
|
|
46
49
|
const anchorKey = previous.rowKeys[anchor.row];
|
|
@@ -11,14 +11,11 @@ import { committedClipPath } from "../../components/editor/cropPresetStore";
|
|
|
11
11
|
import { isAudibleVideoNode } from "../lib/timelineElementHelpers";
|
|
12
12
|
import type { ClipToolState } from "./clipToolAttrs";
|
|
13
13
|
|
|
14
|
-
const LINK_ATTR = "data-link";
|
|
15
|
-
|
|
16
14
|
const WATCHED_ATTRS = [
|
|
17
15
|
HF_AUDIO_FX_ATTR,
|
|
18
16
|
HF_AUDIO_AUTOMATION_ATTR,
|
|
19
17
|
HF_AUDIO_CARVE_ATTR,
|
|
20
18
|
HF_COLOR_GRADING_ATTR,
|
|
21
|
-
LINK_ATTR,
|
|
22
19
|
"data-volume",
|
|
23
20
|
"data-has-audio",
|
|
24
21
|
"muted",
|
|
@@ -42,7 +39,6 @@ function toolStateFromNode(node: Element): ClipToolState {
|
|
|
42
39
|
colorGrading: node.getAttribute(HF_COLOR_GRADING_ATTR),
|
|
43
40
|
clipPath: committedClipPath(node),
|
|
44
41
|
carve: node.getAttribute(HF_AUDIO_CARVE_ATTR),
|
|
45
|
-
link: node.getAttribute(LINK_ATTR),
|
|
46
42
|
};
|
|
47
43
|
}
|
|
48
44
|
|
|
@@ -58,7 +54,6 @@ function toolStateFromElement(el: TimelineElement): ClipToolState {
|
|
|
58
54
|
colorGrading: null,
|
|
59
55
|
clipPath: null,
|
|
60
56
|
carve: null,
|
|
61
|
-
link: null,
|
|
62
57
|
};
|
|
63
58
|
}
|
|
64
59
|
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
BlockedClipState,
|
|
28
28
|
} from "./timelineClipDragTypes";
|
|
29
29
|
import { getTimelineElementIndexes } from "../lib/timelineElementIndexes";
|
|
30
|
-
import { linkedGestureKeys } from "./audioClipLink";
|
|
30
|
+
import { dropMisalignedTrimPartners, linkedGestureKeys } from "./audioClipLink";
|
|
31
31
|
import { isLinkedSelectionOn } from "../../utils/linkedClipPreferences";
|
|
32
32
|
import type { TimelineRowGeometry } from "./timelineLayout";
|
|
33
33
|
import {
|
|
@@ -242,12 +242,17 @@ export function useTimelineClipDrag({
|
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
244
|
beginGesture("resize", next.pointerId);
|
|
245
|
-
gestureSelectedKeysRef.current =
|
|
246
|
-
|
|
245
|
+
gestureSelectedKeysRef.current = dropMisalignedTrimPartners(
|
|
246
|
+
linkedGestureKeys(
|
|
247
|
+
gestureSelectedKeysRef.current,
|
|
248
|
+
next.element,
|
|
249
|
+
elementsRef.current,
|
|
250
|
+
next.altKey === true,
|
|
251
|
+
isLinkedSelectionOn(),
|
|
252
|
+
),
|
|
247
253
|
next.element,
|
|
248
254
|
elementsRef.current,
|
|
249
|
-
next.
|
|
250
|
-
isLinkedSelectionOn(),
|
|
255
|
+
next.edge,
|
|
251
256
|
);
|
|
252
257
|
publishResizingClip(next);
|
|
253
258
|
},
|
|
@@ -324,6 +329,8 @@ export function useTimelineClipDrag({
|
|
|
324
329
|
scroll: scrollRef.current,
|
|
325
330
|
pps: ppsRef.current,
|
|
326
331
|
buildSnapTargets,
|
|
332
|
+
elements: elementsRef.current,
|
|
333
|
+
gestureKeys: gestureSelectedKeysRef.current,
|
|
327
334
|
});
|
|
328
335
|
trimSeekOriginRef.current ??= usePlayerStore.getState().currentTime;
|
|
329
336
|
const setResizeState = (v: ResizePreviewResult) => {
|
|
@@ -32,7 +32,6 @@ type OverlayStateInputs = Omit<
|
|
|
32
32
|
|
|
33
33
|
export type TimelineOverlaysStateResult = {
|
|
34
34
|
overlays: TimelineOverlaysState;
|
|
35
|
-
shiftClickClipRef: ReturnType<typeof useTimelineRangeSelection>["shiftClickClipRef"];
|
|
36
35
|
marqueeRect: ReturnType<typeof useTimelineRangeSelection>["marqueeRect"];
|
|
37
36
|
isScrubbing: ReturnType<typeof useTimelineRangeSelection>["isScrubbing"];
|
|
38
37
|
handlePointerDown: ReturnType<typeof useTimelineRangeSelection>["handlePointerDown"];
|
|
@@ -55,7 +54,6 @@ export function useTimelineOverlaysState({
|
|
|
55
54
|
const {
|
|
56
55
|
rangeSelection,
|
|
57
56
|
setRangeSelection,
|
|
58
|
-
shiftClickClipRef,
|
|
59
57
|
marqueeRect,
|
|
60
58
|
isScrubbing,
|
|
61
59
|
handlePointerDown,
|
|
@@ -86,7 +84,6 @@ export function useTimelineOverlaysState({
|
|
|
86
84
|
onCloseAllTrackGaps: gap.closeAllTrackGaps,
|
|
87
85
|
onHoverGapAction: gap.setHoveredGapAction,
|
|
88
86
|
},
|
|
89
|
-
shiftClickClipRef,
|
|
90
87
|
marqueeRect,
|
|
91
88
|
isScrubbing,
|
|
92
89
|
handlePointerDown,
|
|
@@ -420,7 +420,6 @@ export function useTimelineProviderState({
|
|
|
420
420
|
gapHighlight,
|
|
421
421
|
openGapMenu,
|
|
422
422
|
onContextMenuClip,
|
|
423
|
-
shiftClickClipRef,
|
|
424
423
|
marqueeRect,
|
|
425
424
|
isScrubbing,
|
|
426
425
|
handlePointerDown,
|
|
@@ -503,7 +502,6 @@ export function useTimelineProviderState({
|
|
|
503
502
|
setResizingClip,
|
|
504
503
|
setDraggedClip,
|
|
505
504
|
setSelectedElementId,
|
|
506
|
-
shiftClickClipRef,
|
|
507
505
|
getPreviewElement,
|
|
508
506
|
getTrackStyle,
|
|
509
507
|
keyframeCache,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useRef, useState, useCallback, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
|
-
buildClipRangeSelection,
|
|
4
3
|
applyTimelineAutoScrollStep,
|
|
5
4
|
resolveTimelineAutoScrollLoopAction,
|
|
6
5
|
type TimelineRangeSelection,
|
|
@@ -149,11 +148,6 @@ export function useTimelineRangeSelection({
|
|
|
149
148
|
// rendering). Drives the playhead head's filled-vs-hollow state.
|
|
150
149
|
const [isScrubbing, setIsScrubbing] = useState(false);
|
|
151
150
|
const [rangeSelection, setRangeSelection] = useState<TimelineRangeSelection | null>(null);
|
|
152
|
-
const shiftClickClipRef = useRef<{
|
|
153
|
-
element: TimelineElement;
|
|
154
|
-
anchorX: number;
|
|
155
|
-
anchorY: number;
|
|
156
|
-
} | null>(null);
|
|
157
151
|
|
|
158
152
|
const seekRafRef = useRef(0);
|
|
159
153
|
const pendingClientXRef = useRef(0);
|
|
@@ -322,7 +316,6 @@ export function useTimelineRangeSelection({
|
|
|
322
316
|
beginRangeSelection(e);
|
|
323
317
|
return;
|
|
324
318
|
}
|
|
325
|
-
shiftClickClipRef.current = null;
|
|
326
319
|
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
327
320
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
328
321
|
activePointerIdRef.current = e.pointerId;
|
|
@@ -427,13 +420,7 @@ export function useTimelineRangeSelection({
|
|
|
427
420
|
// clip range), otherwise clear it.
|
|
428
421
|
const finishRangeSelection = useCallback(() => {
|
|
429
422
|
isRangeSelecting.current = false;
|
|
430
|
-
const pendingShiftClick = shiftClickClipRef.current;
|
|
431
|
-
shiftClickClipRef.current = null;
|
|
432
423
|
setRangeSelection((prev) => {
|
|
433
|
-
if (prev && pendingShiftClick && Math.abs(prev.end - prev.start) <= 0.2) {
|
|
434
|
-
setShowPopover(true);
|
|
435
|
-
return buildClipRangeSelection(pendingShiftClick.element, pendingShiftClick);
|
|
436
|
-
}
|
|
437
424
|
if (prev && Math.abs(prev.end - prev.start) > 0.2) {
|
|
438
425
|
setShowPopover(true);
|
|
439
426
|
return prev;
|
|
@@ -589,7 +576,6 @@ export function useTimelineRangeSelection({
|
|
|
589
576
|
return {
|
|
590
577
|
rangeSelection,
|
|
591
578
|
setRangeSelection,
|
|
592
|
-
shiftClickClipRef,
|
|
593
579
|
marqueeRect,
|
|
594
580
|
isScrubbing,
|
|
595
581
|
handlePointerDown,
|
|
@@ -120,6 +120,7 @@ export function useTimelineRowVirtualization({
|
|
|
120
120
|
previousGeometry,
|
|
121
121
|
rowGeometry,
|
|
122
122
|
scroll.scrollTop,
|
|
123
|
+
draggedRowKey !== undefined,
|
|
123
124
|
);
|
|
124
125
|
if (nextScrollTop !== scroll.scrollTop) {
|
|
125
126
|
scroll.scrollTop = nextScrollTop;
|
|
@@ -127,7 +128,7 @@ export function useTimelineRowVirtualization({
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
previousLayoutRef.current = rowGeometry;
|
|
130
|
-
}, [lastScrollLeftRef, rowGeometry, scrollRef, sessionEpoch, syncScrollViewport]);
|
|
131
|
+
}, [draggedRowKey, lastScrollLeftRef, rowGeometry, scrollRef, sessionEpoch, syncScrollViewport]);
|
|
131
132
|
|
|
132
133
|
return {
|
|
133
134
|
enabled,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { openComposition } from "@hyperframes/sdk";
|
|
3
|
+
import { cutoverCommittedOrThrow, sdkTimingBatchPersist } from "./sdkCutover";
|
|
4
|
+
import { getStudioSaveErrorMessage } from "./studioSaveDiagnostics";
|
|
5
|
+
|
|
6
|
+
vi.mock("../components/editor/manualEditingAvailability", () => ({
|
|
7
|
+
STUDIO_SDK_CUTOVER_ENABLED: true,
|
|
8
|
+
STUDIO_SDK_RESOLVER_SHADOW_ENABLED: false,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("./studioTelemetry", () => ({
|
|
11
|
+
trackStudioEvent: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const OFFSET_LINKED_HTML = `
|
|
15
|
+
<div data-hf-id="hf-stage" data-hf-root style="width:1280px;height:720px" data-duration="10">
|
|
16
|
+
<video data-hf-id="hf-talk" src="talk.mp4" muted data-link="lk-1" data-start="2" data-duration="6" data-track-index="0"></video>
|
|
17
|
+
<audio data-hf-id="hf-talk-audio" src="talk.mp4" data-link="lk-1" data-start="3" data-duration="5" data-track-index="2"></audio>
|
|
18
|
+
</div>
|
|
19
|
+
`.trim();
|
|
20
|
+
|
|
21
|
+
describe("a linked trim the SDK refuses", () => {
|
|
22
|
+
it("fails the cutover with the SDK's reason, which the resize toast shows, and writes nothing", async () => {
|
|
23
|
+
const session = await openComposition(OFFSET_LINKED_HTML);
|
|
24
|
+
const before = session.serialize();
|
|
25
|
+
const deps = {
|
|
26
|
+
editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
|
|
27
|
+
writeProjectFile: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
reloadPreview: vi.fn(),
|
|
29
|
+
publishSession: vi.fn(),
|
|
30
|
+
createCandidateSession: async () => session,
|
|
31
|
+
};
|
|
32
|
+
const result = await sdkTimingBatchPersist(
|
|
33
|
+
[{ hfId: "hf-talk", timingUpdate: { start: 2, duration: 0.5 } }],
|
|
34
|
+
"index.html",
|
|
35
|
+
session,
|
|
36
|
+
deps,
|
|
37
|
+
);
|
|
38
|
+
expect(result.status).toBe("failed");
|
|
39
|
+
let thrown: unknown;
|
|
40
|
+
try {
|
|
41
|
+
cutoverCommittedOrThrow(result);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
thrown = error;
|
|
44
|
+
}
|
|
45
|
+
expect(getStudioSaveErrorMessage(thrown)).toBe(
|
|
46
|
+
"Linked audio would start after the new end — unlink or trim the audio first.",
|
|
47
|
+
);
|
|
48
|
+
expect(deps.writeProjectFile).not.toHaveBeenCalled();
|
|
49
|
+
expect(deps.editHistory.recordEdit).not.toHaveBeenCalled();
|
|
50
|
+
expect(session.serialize()).toBe(before);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
trackedStudioEdit,
|
|
11
11
|
} from "./studioPendingEdits";
|
|
12
12
|
import { StudioFileConflictError } from "./studioSaveDiagnostics";
|
|
13
|
+
import { revertNewestStudioPendingEdit as hostRevert } from "../index";
|
|
13
14
|
|
|
14
15
|
describe("studio pending edit flush", () => {
|
|
15
16
|
it("waits for mounted panels to persist pending local edits", async () => {
|
|
@@ -277,3 +278,20 @@ describe("a pending edit whose start throws", () => {
|
|
|
277
278
|
expect(hasStudioPendingEdits()).toBe(false);
|
|
278
279
|
});
|
|
279
280
|
});
|
|
281
|
+
|
|
282
|
+
describe("the package's public revert", () => {
|
|
283
|
+
it("lets a host's own undo key paint a still-saving move back at once", async () => {
|
|
284
|
+
let left = "120px";
|
|
285
|
+
const edit = beginStudioPendingEdit(() => ((left = "0px"), () => void (left = "120px")));
|
|
286
|
+
try {
|
|
287
|
+
const putBack = hostRevert();
|
|
288
|
+
expect(left).toBe("0px");
|
|
289
|
+
expect(hostRevert()).toBeNull();
|
|
290
|
+
putBack?.();
|
|
291
|
+
expect(left).toBe("120px");
|
|
292
|
+
} finally {
|
|
293
|
+
edit.settle();
|
|
294
|
+
await flushStudioPendingEdits();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -54,6 +54,14 @@ describe("isTypingTarget", () => {
|
|
|
54
54
|
expect(isTypingTarget(mount('<h1 contenteditable="false">Hi</h1>'))).toBe(false);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
it("keeps a modal dialog's buttons out of the global shortcuts", () => {
|
|
58
|
+
const host = mount('<div role="dialog" aria-modal="true"><button>OK</button></div>');
|
|
59
|
+
expect(isTypingTarget(host.querySelector("button"))).toBe(true);
|
|
60
|
+
expect(
|
|
61
|
+
isTypingTarget(mount('<div role="dialog"><button>OK</button></div>').querySelector("button")),
|
|
62
|
+
).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
57
65
|
it("says no to nothing at all", () => {
|
|
58
66
|
expect(isTypingTarget(null)).toBe(false);
|
|
59
67
|
expect(isTypingTarget({} as EventTarget)).toBe(false);
|
|
@@ -45,6 +45,7 @@ const TYPING_SELECTOR = [
|
|
|
45
45
|
// `input` and matched above. Without this row the shared Toggle would let a
|
|
46
46
|
// global shortcut claim Space instead of flipping the control (KTD13).
|
|
47
47
|
"[role='switch']",
|
|
48
|
+
"[role='dialog'][aria-modal='true']",
|
|
48
49
|
".cm-editor",
|
|
49
50
|
].join(",");
|
|
50
51
|
|