@hyperframes/studio 0.8.85 → 0.8.87
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-CtNVImMi.js +473 -0
- package/dist/assets/{index-Bus1BKtk.js → index-B_MF86RE.js} +1 -1
- package/dist/assets/{index-CZxhjQpw.js → index-DJ-Ol7YS.js} +1 -1
- package/dist/assets/{index-ClLdmgua.js → index-DZKcDAtf.js} +203 -203
- package/dist/assets/{index-mzZCha_N.css → index-Dx79V1Se.css} +1 -1
- package/dist/{chunk-7VDQIGOU.js → chunk-7PLTC6SV.js} +8 -2
- package/dist/chunk-7PLTC6SV.js.map +1 -0
- package/dist/{chunk-3IOCVWB6.js → chunk-PQFEGZWM.js} +3 -1
- package/dist/chunk-PQFEGZWM.js.map +1 -0
- package/dist/{domEditingLayers-ZOOMN7CQ.js → domEditingLayers-3BAP6NNO.js} +2 -2
- package/dist/index.d.ts +207 -161
- package/dist/index.html +2 -2
- package/dist/index.js +2084 -1705
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/TimelineToolbar.test.tsx +53 -2
- package/src/components/TimelineToolbar.tsx +119 -101
- package/src/components/editor/DomEditOverlay.tsx +1 -2
- package/src/components/editor/DomEditOverlayHover.test.tsx +5 -0
- package/src/components/editor/DomEditSelectionChrome.test.tsx +0 -3
- package/src/components/editor/DomEditSelectionChrome.tsx +3 -13
- package/src/components/editor/LayersPanel.test.ts +14 -0
- package/src/components/editor/LayersPanel.tsx +2 -2
- package/src/components/editor/TopologyLens.tsx +2 -2
- package/src/components/editor/commitPreflightHandles.test.tsx +466 -0
- package/src/components/editor/domEditOverlayGestures.ts +0 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +10 -1
- package/src/components/editor/domEditing.test.ts +29 -0
- package/src/components/editor/domEditingDom.ts +1 -0
- package/src/components/editor/domEditingTypes.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +84 -0
- package/src/components/editor/marqueeCommit.ts +13 -1
- package/src/components/editor/useColorGradingController.test.ts +24 -0
- package/src/components/editor/useColorGradingController.ts +3 -2
- package/src/components/editor/useDomEditOverlayGestures.ts +20 -5
- package/src/components/nle/useCompositionStack.test.tsx +179 -0
- package/src/components/nle/useCompositionStack.ts +26 -20
- package/src/hooks/domStyleCommit.ts +129 -0
- package/src/hooks/gsapEditOutcome.ts +2 -2
- package/src/hooks/gsapElementMatch.ts +38 -0
- package/src/hooks/gsapResizeIntercept.ts +10 -31
- package/src/hooks/gsapResizePreflight.ts +56 -0
- package/src/hooks/gsapRuntimeBridge.ts +66 -26
- package/src/hooks/keyframeCacheAstLoad.ts +4 -1
- package/src/hooks/useCommitPreflightCapabilities.ts +147 -0
- package/src/hooks/useDomEditCommits.ts +17 -182
- package/src/hooks/useDomEditPersist.ts +225 -0
- package/src/hooks/useDomEditPreviewSync.test.ts +58 -0
- package/src/hooks/useDomEditPreviewSync.ts +2 -4
- package/src/hooks/useDomEditSession.test.tsx +51 -1
- package/src/hooks/useDomEditSession.ts +11 -2
- package/src/hooks/useDomEditTextCommits.ts +15 -99
- package/src/hooks/useDomStyleCommit.test.tsx +155 -0
- package/src/hooks/useDomStyleCommit.ts +74 -0
- package/src/hooks/useGsapAnimationFetchFallback.test.ts +12 -0
- package/src/hooks/useGsapAnimationFetchFallback.ts +8 -2
- package/src/hooks/useGsapAwareEditing.test.tsx +4 -0
- package/src/hooks/useGsapAwareEditing.ts +1 -1
- package/src/hooks/useGsapTweenCache.ts +2 -48
- package/src/index.ts +10 -1
- package/src/player/components/Timeline.audioEffects.test.tsx +80 -0
- package/src/player/components/Timeline.bandEscape.test.tsx +75 -0
- package/src/player/components/Timeline.playheadRemount.test.tsx +41 -0
- package/src/player/components/Timeline.test.ts +32 -19
- package/src/player/components/TimelineGroupHeader.tsx +21 -10
- package/src/player/components/TimelineGroupLaneLabels.tsx +7 -2
- package/src/player/components/TimelineGroupRow.test.tsx +24 -33
- package/src/player/components/TimelineGroupRow.tsx +4 -1
- package/src/player/components/TimelineLanes.tsx +5 -5
- package/src/player/components/TimelineTrackHeader.test.tsx +27 -0
- package/src/player/components/TimelineTrackHeader.tsx +6 -3
- package/src/player/components/TimelineTypes.ts +1 -0
- package/src/player/components/timelineLaneProps.ts +1 -0
- package/src/player/components/timelineLayout.ts +16 -23
- package/src/player/components/timelineMountTestEnv.ts +29 -0
- package/src/player/components/useTimelineGeometry.test.tsx +105 -0
- package/src/player/components/useTimelineGeometry.ts +18 -10
- package/src/player/components/useTimelineLabelColumn.ts +18 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +6 -1
- package/src/player/components/useTimelinePlayhead.ts +1 -1
- package/src/player/components/useTimelineProviderState.tsx +6 -12
- package/src/player/components/useTimelineRangeSelection.ts +13 -12
- package/src/player/hooks/useShadowPreviewReload.ts +2 -0
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +191 -0
- package/src/player/hooks/useTimelinePlayer.ts +29 -7
- package/src/player/lib/runtimeProtocol.test.ts +1 -0
- package/src/player/sceneSwap.test.ts +95 -0
- package/src/player/sceneSwap.ts +53 -0
- package/src/player/store/liveTime.ts +6 -1
- package/dist/assets/hyperframes-player-BK18SggX.js +0 -464
- package/dist/chunk-3IOCVWB6.js.map +0 -1
- package/dist/chunk-7VDQIGOU.js.map +0 -1
- /package/dist/{domEditingLayers-ZOOMN7CQ.js.map → domEditingLayers-3BAP6NNO.js.map} +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { SCENE_SWAP_MS, sceneSwapFor } from "./sceneSwap";
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.useRealTimers();
|
|
7
|
+
vi.restoreAllMocks();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
type Swap = (html: string, signal?: AbortSignal) => Promise<void>;
|
|
11
|
+
|
|
12
|
+
function swappableIframe(swap: Swap = vi.fn(async () => {})) {
|
|
13
|
+
const iframe = document.createElement("iframe");
|
|
14
|
+
document.body.append(iframe);
|
|
15
|
+
Object.assign(iframe.contentWindow as object, { __hfSwapScenes: swap });
|
|
16
|
+
return { iframe, swap };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("sceneSwapFor", () => {
|
|
20
|
+
it("gives up on a preview download that never finishes, so the caller can reload", async () => {
|
|
21
|
+
vi.useFakeTimers();
|
|
22
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
23
|
+
(_url, init) =>
|
|
24
|
+
new Promise((_resolve, reject) =>
|
|
25
|
+
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason)),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
const { iframe, swap } = swappableIframe();
|
|
29
|
+
const swapping = sceneSwapFor(iframe)!("/preview", () => true);
|
|
30
|
+
const outcome = expect(swapping).rejects.toThrow("took too long");
|
|
31
|
+
await vi.advanceTimersByTimeAsync(SCENE_SWAP_MS);
|
|
32
|
+
await outcome;
|
|
33
|
+
expect(swap).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.each([
|
|
37
|
+
["a body that never finishes", async () => new Response(new ReadableStream())],
|
|
38
|
+
["a runtime swap that never finishes", async () => new Response("<html></html>")],
|
|
39
|
+
])("gives up on %s, so the caller can reload", async (_stall, respond) => {
|
|
40
|
+
vi.useFakeTimers();
|
|
41
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(respond);
|
|
42
|
+
const { iframe } = swappableIframe(vi.fn(() => new Promise<void>(() => {})));
|
|
43
|
+
const swapped = vi.fn();
|
|
44
|
+
iframe.addEventListener("hf-scenes-swapped", swapped);
|
|
45
|
+
const outcome = expect(sceneSwapFor(iframe)!("/preview", () => true)).rejects.toThrow(
|
|
46
|
+
"took too long",
|
|
47
|
+
);
|
|
48
|
+
await vi.advanceTimersByTimeAsync(SCENE_SWAP_MS);
|
|
49
|
+
await outcome;
|
|
50
|
+
expect(swapped).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("cancels the runtime's swap as it gives up, so captions arriving later replace nothing", async () => {
|
|
54
|
+
vi.useFakeTimers();
|
|
55
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("<html></html>"));
|
|
56
|
+
let captionsArrive = () => {};
|
|
57
|
+
const captions = new Promise<void>((resolve) => (captionsArrive = resolve));
|
|
58
|
+
let replaced = false;
|
|
59
|
+
// As the runtime does: it waits for the caption overrides, then refuses a cancelled swap.
|
|
60
|
+
const { iframe } = swappableIframe(async (_html, signal) => {
|
|
61
|
+
await captions;
|
|
62
|
+
if (signal?.aborted) throw new Error("the swap was cancelled");
|
|
63
|
+
replaced = true;
|
|
64
|
+
});
|
|
65
|
+
const outcome = expect(sceneSwapFor(iframe)!("/preview", () => true)).rejects.toThrow(
|
|
66
|
+
"took too long",
|
|
67
|
+
);
|
|
68
|
+
await vi.advanceTimersByTimeAsync(SCENE_SWAP_MS);
|
|
69
|
+
await outcome;
|
|
70
|
+
captionsArrive();
|
|
71
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
72
|
+
expect(replaced).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("does not start a swap whose cancel signal was aborted before the call", async () => {
|
|
76
|
+
const fetchSpy = vi
|
|
77
|
+
.spyOn(globalThis, "fetch")
|
|
78
|
+
.mockImplementation(async () => new Response("<html></html>"));
|
|
79
|
+
const { iframe, swap } = swappableIframe();
|
|
80
|
+
const cancel = new AbortController();
|
|
81
|
+
cancel.abort(new Error("superseded by a newer edit"));
|
|
82
|
+
await expect(sceneSwapFor(iframe)!("/preview", () => true, cancel.signal)).rejects.toThrow(
|
|
83
|
+
"superseded",
|
|
84
|
+
);
|
|
85
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
86
|
+
expect(swap).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("swaps a document that arrives in time", async () => {
|
|
90
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("<html></html>"));
|
|
91
|
+
const { iframe, swap } = swappableIframe();
|
|
92
|
+
await sceneSwapFor(iframe)!("/preview", () => true);
|
|
93
|
+
expect(swap).toHaveBeenCalledWith("<html></html>", expect.anything());
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
type SwapWindow = Window & {
|
|
2
|
+
__hfSwapScenes?: (html: string, signal?: AbortSignal) => Promise<void>;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
const SCENES_SWAPPED = "hf-scenes-swapped";
|
|
6
|
+
export const SCENE_SWAP_MS = 5000;
|
|
7
|
+
|
|
8
|
+
export function onPreviewContentReplaced(
|
|
9
|
+
iframe: HTMLIFrameElement,
|
|
10
|
+
onReplaced: () => void,
|
|
11
|
+
): () => void {
|
|
12
|
+
iframe.addEventListener("load", onReplaced);
|
|
13
|
+
iframe.addEventListener(SCENES_SWAPPED, onReplaced);
|
|
14
|
+
return () => {
|
|
15
|
+
iframe.removeEventListener("load", onReplaced);
|
|
16
|
+
iframe.removeEventListener(SCENES_SWAPPED, onReplaced);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Null when the preview cannot swap; rejects when superseded, cancelled or a full reload is needed. */
|
|
21
|
+
export function sceneSwapFor(
|
|
22
|
+
iframe: HTMLIFrameElement,
|
|
23
|
+
): ((url: string, isCurrent: () => boolean, cancel?: AbortSignal) => Promise<void>) | null {
|
|
24
|
+
const win = iframe.contentWindow as SwapWindow | null;
|
|
25
|
+
const swap = win?.__hfSwapScenes;
|
|
26
|
+
if (typeof swap !== "function") return null;
|
|
27
|
+
return async (url, isCurrent, cancel) => {
|
|
28
|
+
if (cancel?.aborted) throw cancel.reason;
|
|
29
|
+
const deadline = new AbortController();
|
|
30
|
+
const timer = setTimeout(
|
|
31
|
+
() => deadline.abort(new Error("the preview took too long to swap")),
|
|
32
|
+
SCENE_SWAP_MS,
|
|
33
|
+
);
|
|
34
|
+
cancel?.addEventListener("abort", () => deadline.abort(cancel.reason), { once: true });
|
|
35
|
+
const swapping = (async () => {
|
|
36
|
+
const response = await fetch(url, { signal: deadline.signal });
|
|
37
|
+
if (!response.ok) throw new Error(`preview request failed with ${response.status}`);
|
|
38
|
+
const html = await response.text();
|
|
39
|
+
if (!isCurrent()) throw new Error("superseded by a newer reload");
|
|
40
|
+
// Aborted with the deadline, so a swap still waiting cannot land after the reload has begun.
|
|
41
|
+
await swap.call(win, html, deadline.signal);
|
|
42
|
+
})();
|
|
43
|
+
const expired = new Promise<never>((_resolve, reject) =>
|
|
44
|
+
deadline.signal.addEventListener("abort", () => reject(deadline.signal.reason)),
|
|
45
|
+
);
|
|
46
|
+
try {
|
|
47
|
+
await Promise.race([swapping, expired]);
|
|
48
|
+
} finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
iframe.dispatchEvent(new Event(SCENES_SWAPPED));
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// ponytail: Playback RAF updates the playhead/time display without per-frame React renders.
|
|
2
2
|
type TimeListener = (time: number) => void;
|
|
3
3
|
const timeListeners = new Set<TimeListener>();
|
|
4
|
+
let latestTime = 0;
|
|
4
5
|
|
|
5
6
|
export const liveTime = {
|
|
6
|
-
notify: (time: number) =>
|
|
7
|
+
notify: (time: number) => {
|
|
8
|
+
latestTime = time;
|
|
9
|
+
timeListeners.forEach((listener) => listener(time));
|
|
10
|
+
},
|
|
11
|
+
latest: () => latestTime,
|
|
7
12
|
subscribe: (listener: TimeListener) => {
|
|
8
13
|
timeListeners.add(listener);
|
|
9
14
|
return () => timeListeners.delete(listener);
|