@hyperframes/studio 0.7.56 → 0.7.58
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-CtTDO63S.js +459 -0
- package/dist/assets/{index-CmVCjZjp.js → index-B_UvTX3E.js} +205 -205
- package/dist/assets/{index-BWnOxAiH.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-C4csZims.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +41 -4
- package/dist/index.html +2 -2
- package/dist/index.js +7436 -5423
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +166 -2
- package/src/components/editor/CanvasContextMenu.tsx +100 -20
- package/src/components/editor/DomEditOverlay.tsx +34 -62
- package/src/components/editor/LayersPanel.tsx +111 -91
- package/src/components/editor/canvasContextMenuZOrder.test.ts +243 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +184 -23
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +61 -37
- package/src/components/nle/PreviewOverlays.tsx +61 -16
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +19 -2
- package/src/components/sidebar/AudioRow.tsx +19 -2
- package/src/hooks/domEditCommitTypes.ts +29 -2
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +183 -1
- package/src/hooks/timelineEditingHelpers.ts +63 -259
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +7 -4
- package/src/hooks/timelineTimingSync.test.ts +820 -0
- package/src/hooks/timelineTimingSync.ts +597 -0
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +392 -23
- package/src/hooks/useDomEditCommits.ts +180 -53
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +559 -28
- package/src/hooks/useElementLifecycleOps.ts +175 -69
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +591 -277
- package/src/hooks/useTimelineEditing.ts +202 -282
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +187 -134
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +105 -18
- package/src/player/components/Timeline.tsx +69 -98
- package/src/player/components/TimelineCanvas.tsx +47 -7
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +70 -47
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +370 -11
- package/src/player/components/timelineClipDragCommit.ts +267 -68
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +119 -30
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +102 -1
- package/src/player/components/timelineStackingSync.ts +45 -5
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -2
- package/src/player/components/useTimelineStackingSync.ts +17 -4
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +19 -1
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +11 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/lib/timelineElementHelpers.ts +54 -11
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +49 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +62 -86
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-D-GyYi2d.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -46,6 +46,7 @@ import { scrubMusicAtSeek, stopScrubPreviewAudio } from "../lib/playbackScrub";
|
|
|
46
46
|
import { applyCachedSourceDurations, probeMissingSourceDurations } from "../lib/mediaProbe";
|
|
47
47
|
import { shouldResumeForwardPlaybackAfterSeek, shouldStopAfterSeek } from "../lib/playbackSeek";
|
|
48
48
|
import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
|
|
49
|
+
import { acceptStudioRuntimeMessage } from "../lib/runtimeProtocol";
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Whether the derived elements differ from the current ones in any field that
|
|
@@ -492,6 +493,9 @@ export function useTimelinePlayer() {
|
|
|
492
493
|
if (e.source && ourIframe && e.source !== ourIframe.contentWindow) {
|
|
493
494
|
return;
|
|
494
495
|
}
|
|
496
|
+
if (data?.source === "hf-preview") {
|
|
497
|
+
if (!acceptStudioRuntimeMessage(data)) return;
|
|
498
|
+
}
|
|
495
499
|
if (data?.source === "hf-preview" && data?.type === "state") {
|
|
496
500
|
try {
|
|
497
501
|
if (usePlayerStore.getState().elements.length === 0) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { useCallback } from "react";
|
|
12
12
|
import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
13
13
|
import type { TimelineElement, DomClipChild } from "../store/playerStore";
|
|
14
|
+
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
15
|
import type { PlaybackAdapter, ClipManifestClip, IframeWindow } from "../lib/playbackTypes";
|
|
15
16
|
import {
|
|
16
17
|
parseTimelineFromDOM,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
autoHealMissingCompositionIds,
|
|
27
28
|
buildMissingCompositionElements,
|
|
28
29
|
} from "../lib/timelineIframeHelpers";
|
|
30
|
+
import { acceptedRuntimeMessageFps, inspectStudioRuntimeMessage } from "../lib/runtimeProtocol";
|
|
29
31
|
|
|
30
32
|
interface UseTimelineSyncCallbacksParams {
|
|
31
33
|
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
@@ -132,6 +134,9 @@ export function useTimelineSyncCallbacks({
|
|
|
132
134
|
clips: ClipManifestClip[];
|
|
133
135
|
durationInFrames: number;
|
|
134
136
|
scenes?: Array<{ id: string; label: string; start: number; duration: number }>;
|
|
137
|
+
protocolVersion?: unknown;
|
|
138
|
+
capabilities?: unknown;
|
|
139
|
+
fps?: unknown;
|
|
135
140
|
}) => {
|
|
136
141
|
if (!data.clips || data.clips.length === 0) {
|
|
137
142
|
return;
|
|
@@ -195,6 +200,7 @@ export function useTimelineSyncCallbacks({
|
|
|
195
200
|
parentId,
|
|
196
201
|
hostId,
|
|
197
202
|
label: isGroup ? child.getAttribute("data-hf-group") || child.id : child.id,
|
|
203
|
+
stackingContextId: resolveCssStackingContextId(child),
|
|
198
204
|
});
|
|
199
205
|
parentMap.set(child.id, parentId);
|
|
200
206
|
if (isGroup) collect(child, child.id);
|
|
@@ -222,7 +228,7 @@ export function useTimelineSyncCallbacks({
|
|
|
222
228
|
hostEl,
|
|
223
229
|
});
|
|
224
230
|
});
|
|
225
|
-
const rawDuration = data.durationInFrames /
|
|
231
|
+
const rawDuration = data.durationInFrames / acceptedRuntimeMessageFps(data);
|
|
226
232
|
// Clamp non-finite or absurdly large durations — the runtime can emit
|
|
227
233
|
// Infinity when it detects a loop-inflated GSAP timeline without an
|
|
228
234
|
// explicit data-duration on the root composition. Floor the manifest total
|
|
@@ -418,6 +424,10 @@ export function useTimelineSyncCallbacks({
|
|
|
418
424
|
if (e.source && iframe && e.source !== iframe.contentWindow) return;
|
|
419
425
|
const data = e.data;
|
|
420
426
|
if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) {
|
|
427
|
+
// The main message handler owns protocol-error diagnostics. This readiness-only
|
|
428
|
+
// listener mirrors its acceptance gate without dispatching a duplicate event:
|
|
429
|
+
// an unsupported runtime must not make the iframe appear successfully settled.
|
|
430
|
+
if (inspectStudioRuntimeMessage(data).status === "unsupported") return;
|
|
421
431
|
trySettle();
|
|
422
432
|
}
|
|
423
433
|
};
|
package/src/player/index.ts
CHANGED
|
@@ -11,7 +11,9 @@ export { resolveIframe } from "./lib/timelineDOM";
|
|
|
11
11
|
|
|
12
12
|
// Store
|
|
13
13
|
export { usePlayerStore, liveTime } from "./store/playerStore";
|
|
14
|
-
|
|
14
|
+
// Public library surface; external consumers are invisible to the workspace analyzer.
|
|
15
|
+
// fallow-ignore-next-line unused-exports
|
|
16
|
+
export type { SelectElementOptions, TimelineElement, ZoomMode } from "./store/playerStore";
|
|
15
17
|
|
|
16
18
|
// Utils
|
|
17
19
|
export { formatTime } from "./lib/time";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import { getElementZIndex, resolveContextOrder } from "./layerOrdering";
|
|
5
5
|
|
|
6
6
|
function makeElement(zIndex?: string): HTMLElement {
|
|
7
7
|
const element = document.createElement("div");
|
|
@@ -31,20 +31,6 @@ describe("getElementZIndex", () => {
|
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
describe("computeReorderZValues", () => {
|
|
35
|
-
it("preserves distinct existing z-index values and remaps them onto the new order", () => {
|
|
36
|
-
expect(computeReorderZValues([1, 8, 3], 0, 2)).toEqual([8, 3, 1]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("renumbers an all-tied group to descending contiguous z-index values", () => {
|
|
40
|
-
expect(computeReorderZValues([0, 0, 0], 2, 0)).toEqual([3, 2, 1]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("renumbers the whole group when any existing z-index values are tied", () => {
|
|
44
|
-
expect(computeReorderZValues([5, 5, 1], 2, 0)).toEqual([3, 2, 1]);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
34
|
describe("resolveContextOrder", () => {
|
|
49
35
|
it("sorts a flat sibling group by z-index descending then original order", () => {
|
|
50
36
|
const ordered = resolveContextOrder([
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readLayerRevealPriorZ } from "./timelineElementHelpers";
|
|
2
|
+
|
|
1
3
|
export interface StackingContextDescriptor {
|
|
2
4
|
parentCompositionId: string | null;
|
|
3
5
|
compositionAncestors: readonly string[];
|
|
@@ -11,6 +13,9 @@ export interface ContextOrderItem extends StackingContextDescriptor {
|
|
|
11
13
|
// fallow-ignore-next-line complexity
|
|
12
14
|
export function getElementZIndex(element: HTMLElement): number {
|
|
13
15
|
try {
|
|
16
|
+
// An active Layers-panel reveal lift reports the element's TRUE z.
|
|
17
|
+
const prior = readLayerRevealPriorZ(element);
|
|
18
|
+
if (prior != null) return prior;
|
|
14
19
|
const inline = element.style?.zIndex;
|
|
15
20
|
if (inline && inline !== "auto") {
|
|
16
21
|
const parsed = parseInt(inline, 10);
|
|
@@ -27,34 +32,6 @@ export function getElementZIndex(element: HTMLElement): number {
|
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
// fallow-ignore-next-line complexity
|
|
31
|
-
export function hasExplicitZIndex(element: HTMLElement): boolean {
|
|
32
|
-
try {
|
|
33
|
-
const inline = element.style?.zIndex;
|
|
34
|
-
if (inline) return inline !== "auto";
|
|
35
|
-
const win = element.ownerDocument?.defaultView;
|
|
36
|
-
if (!win) return false;
|
|
37
|
-
const value = win.getComputedStyle(element).zIndex;
|
|
38
|
-
return value !== "auto" && value !== "";
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function computeReorderZValues(
|
|
45
|
-
existingValues: readonly number[],
|
|
46
|
-
fromIndex: number,
|
|
47
|
-
toIndex: number,
|
|
48
|
-
): number[] {
|
|
49
|
-
const reordered = [...existingValues];
|
|
50
|
-
const [moved] = reordered.splice(fromIndex, 1);
|
|
51
|
-
reordered.splice(toIndex, 0, moved);
|
|
52
|
-
|
|
53
|
-
const sorted = [...existingValues].sort((a, b) => b - a);
|
|
54
|
-
const hasDupes = sorted.some((v, i) => i > 0 && v === sorted[i - 1]);
|
|
55
|
-
return hasDupes ? reordered.map((_, i) => reordered.length - i) : sorted;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
35
|
export function resolveStackingContextKey(item: StackingContextDescriptor): string {
|
|
59
36
|
return item.stackingContextId ?? item.parentCompositionId ?? item.compositionAncestors[0] ?? "";
|
|
60
37
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
acceptStudioRuntimeMessage,
|
|
4
|
+
acceptedRuntimeMessageFps,
|
|
5
|
+
createRuntimeControlMessage,
|
|
6
|
+
inspectStudioRuntimeMessage,
|
|
7
|
+
postRuntimeControlMessage,
|
|
8
|
+
} from "./runtimeProtocol";
|
|
9
|
+
|
|
10
|
+
describe("Studio runtime protocol", () => {
|
|
11
|
+
it("versions every control message and declares rational fps", () => {
|
|
12
|
+
expect(createRuntimeControlMessage("seek", { timeSeconds: 1.25 }, 60)).toEqual({
|
|
13
|
+
source: "hf-parent",
|
|
14
|
+
type: "control",
|
|
15
|
+
action: "seek",
|
|
16
|
+
protocolVersion: 1,
|
|
17
|
+
capabilities: ["seconds-time", "rational-fps", "seek-keep-playing"],
|
|
18
|
+
fps: { numerator: 60, denominator: 1 },
|
|
19
|
+
timeSeconds: 1.25,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("posts the typed message to the target window", () => {
|
|
24
|
+
const target = { postMessage: vi.fn() };
|
|
25
|
+
postRuntimeControlMessage(target as unknown as Window, "pause");
|
|
26
|
+
expect(target.postMessage).toHaveBeenCalledWith(
|
|
27
|
+
expect.objectContaining({ action: "pause", protocolVersion: 1 }),
|
|
28
|
+
"*",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("preserves legacy 30fps messages and rejects unknown majors", () => {
|
|
33
|
+
expect(inspectStudioRuntimeMessage({ source: "hf-preview" })).toEqual({
|
|
34
|
+
status: "legacy",
|
|
35
|
+
fps: 30,
|
|
36
|
+
});
|
|
37
|
+
expect(inspectStudioRuntimeMessage({ protocolVersion: 2 })).toMatchObject({
|
|
38
|
+
status: "unsupported",
|
|
39
|
+
code: "unsupported_protocol_version",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("reads explicit fps for accepted timeline messages", () => {
|
|
44
|
+
const message = createRuntimeControlMessage("pause", {}, 60);
|
|
45
|
+
expect(acceptedRuntimeMessageFps(message)).toBe(60);
|
|
46
|
+
expect(acceptStudioRuntimeMessage(message)).toMatchObject({ status: "supported", fps: 60 });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inspectRuntimeProtocol,
|
|
3
|
+
runtimeProtocolMetadata,
|
|
4
|
+
type RuntimeProtocolInspection,
|
|
5
|
+
} from "@hyperframes/core/runtime/protocol";
|
|
6
|
+
|
|
7
|
+
export type RuntimeControlMessage = {
|
|
8
|
+
source: "hf-parent";
|
|
9
|
+
type: "control";
|
|
10
|
+
action: string;
|
|
11
|
+
} & ReturnType<typeof runtimeProtocolMetadata> &
|
|
12
|
+
Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
export function createRuntimeControlMessage(
|
|
15
|
+
action: string,
|
|
16
|
+
payload: Record<string, unknown> = {},
|
|
17
|
+
fps = 30,
|
|
18
|
+
): RuntimeControlMessage {
|
|
19
|
+
return {
|
|
20
|
+
...payload,
|
|
21
|
+
source: "hf-parent",
|
|
22
|
+
type: "control",
|
|
23
|
+
action,
|
|
24
|
+
...runtimeProtocolMetadata(fps),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function postRuntimeControlMessage(
|
|
29
|
+
target: Pick<Window, "postMessage"> | null | undefined,
|
|
30
|
+
action: string,
|
|
31
|
+
payload: Record<string, unknown> = {},
|
|
32
|
+
fps = 30,
|
|
33
|
+
): void {
|
|
34
|
+
target?.postMessage(createRuntimeControlMessage(action, payload, fps), "*");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function inspectStudioRuntimeMessage(value: unknown): RuntimeProtocolInspection {
|
|
38
|
+
return inspectRuntimeProtocol(value, 30);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dispatchRuntimeProtocolError(inspection: RuntimeProtocolInspection): void {
|
|
42
|
+
if (inspection.status !== "unsupported") return;
|
|
43
|
+
window.dispatchEvent(
|
|
44
|
+
new CustomEvent("runtimeprotocolerror", {
|
|
45
|
+
detail: {
|
|
46
|
+
code: inspection.code,
|
|
47
|
+
receivedVersion: inspection.receivedVersion,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function acceptStudioRuntimeMessage(
|
|
54
|
+
value: unknown,
|
|
55
|
+
): Exclude<RuntimeProtocolInspection, { status: "unsupported" }> | null {
|
|
56
|
+
const inspection = inspectStudioRuntimeMessage(value);
|
|
57
|
+
if (inspection.status !== "unsupported") return inspection;
|
|
58
|
+
dispatchRuntimeProtocolError(inspection);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function acceptedRuntimeMessageFps(value: unknown): number {
|
|
63
|
+
const inspection = inspectStudioRuntimeMessage(value);
|
|
64
|
+
return inspection.status === "supported" ? inspection.fps : 30;
|
|
65
|
+
}
|
|
@@ -110,7 +110,79 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
describe("createTimelineElementFromManifestClip — source-scoped selector identity", () => {
|
|
114
|
+
it("ignores an index.html duplicate when indexing a scene.html selector", () => {
|
|
115
|
+
const doc = makeDoc(`
|
|
116
|
+
<div data-composition-id="root" data-composition-file="index.html">
|
|
117
|
+
<div class="sub"></div>
|
|
118
|
+
<div data-composition-id="scene" data-composition-file="scene.html">
|
|
119
|
+
<div class="sub"></div>
|
|
120
|
+
<div class="sub" data-target></div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
`);
|
|
124
|
+
const target = doc.querySelector("[data-target]");
|
|
125
|
+
if (!target) throw new Error("missing target");
|
|
126
|
+
|
|
127
|
+
const element = createTimelineElementFromManifestClip({
|
|
128
|
+
clip: {
|
|
129
|
+
id: null,
|
|
130
|
+
label: "Sub",
|
|
131
|
+
kind: "element",
|
|
132
|
+
tagName: "div",
|
|
133
|
+
start: 0,
|
|
134
|
+
duration: 5,
|
|
135
|
+
track: 0,
|
|
136
|
+
compositionId: null,
|
|
137
|
+
parentCompositionId: null,
|
|
138
|
+
compositionSrc: null,
|
|
139
|
+
assetUrl: null,
|
|
140
|
+
},
|
|
141
|
+
fallbackIndex: 0,
|
|
142
|
+
doc,
|
|
143
|
+
hostEl: target,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(element.sourceFile).toBe("scene.html");
|
|
147
|
+
expect(element.selectorIndex).toBe(1);
|
|
148
|
+
expect(element.key).toBe("scene.html:.sub:1");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
113
152
|
describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
|
|
153
|
+
it("uses the runtime root paint scope for implicit siblings of manifest clips", () => {
|
|
154
|
+
const doc = makeDoc(`
|
|
155
|
+
<div data-composition-id="root">
|
|
156
|
+
<div id="timed" data-start="0" data-duration="5"></div>
|
|
157
|
+
<div id="implicit"></div>
|
|
158
|
+
</div>
|
|
159
|
+
`);
|
|
160
|
+
const timedHost = doc.getElementById("timed");
|
|
161
|
+
const timed = createTimelineElementFromManifestClip({
|
|
162
|
+
clip: {
|
|
163
|
+
id: "timed",
|
|
164
|
+
label: "Timed",
|
|
165
|
+
start: 0,
|
|
166
|
+
duration: 5,
|
|
167
|
+
track: 0,
|
|
168
|
+
stackingContextId: "css:root",
|
|
169
|
+
kind: "element",
|
|
170
|
+
tagName: "div",
|
|
171
|
+
compositionId: null,
|
|
172
|
+
parentCompositionId: null,
|
|
173
|
+
compositionSrc: null,
|
|
174
|
+
assetUrl: null,
|
|
175
|
+
},
|
|
176
|
+
fallbackIndex: 0,
|
|
177
|
+
doc,
|
|
178
|
+
hostEl: timedHost,
|
|
179
|
+
});
|
|
180
|
+
const implicit = createImplicitTimelineLayersFromDOM(doc, 5, [timed])[0];
|
|
181
|
+
|
|
182
|
+
expect(implicit?.stackingContextId).toBe("css:root");
|
|
183
|
+
expect(implicit?.stackingContextId).toBe(timed.stackingContextId);
|
|
184
|
+
});
|
|
185
|
+
|
|
114
186
|
it("harvests hfId from an implicit layer child that has data-hf-id", () => {
|
|
115
187
|
const doc = makeDoc(`
|
|
116
188
|
<div data-composition-id="root">
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { TimelineElement } from "../store/playerStore";
|
|
12
12
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
13
|
+
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
13
14
|
import {
|
|
14
15
|
resolveMediaElement,
|
|
15
16
|
applyMediaMetadataFromElement,
|
|
@@ -113,6 +114,15 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
113
114
|
start: clip.start,
|
|
114
115
|
duration: clip.duration,
|
|
115
116
|
track: clip.track,
|
|
117
|
+
// clip.track IS the authored data-track-index verbatim (the runtime honors
|
|
118
|
+
// it; see parseAuthoredTrack in core/runtime/timeline.ts). Record it at this
|
|
119
|
+
// translation boundary so later display-lane remaps (normalizeToZones,
|
|
120
|
+
// expanded-child rows) can persist in AUTHORED space instead of
|
|
121
|
+
// reconstructing it from lane occupants.
|
|
122
|
+
authoredTrack: clip.track,
|
|
123
|
+
// Runtime-computed stacking context — authoritative; helpers read it, never
|
|
124
|
+
// re-derive it.
|
|
125
|
+
stackingContextId: clip.stackingContextId ?? null,
|
|
116
126
|
domId,
|
|
117
127
|
hfId,
|
|
118
128
|
selector,
|
|
@@ -219,6 +229,7 @@ export function createImplicitTimelineLayersFromDOM(
|
|
|
219
229
|
selector,
|
|
220
230
|
selectorIndex,
|
|
221
231
|
sourceFile,
|
|
232
|
+
stackingContextId: resolveCssStackingContextId(child),
|
|
222
233
|
start: 0,
|
|
223
234
|
tag: child.tagName.toLowerCase(),
|
|
224
235
|
timingSource: "implicit",
|
|
@@ -294,6 +305,7 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
|
|
|
294
305
|
selector,
|
|
295
306
|
selectorIndex,
|
|
296
307
|
sourceFile,
|
|
308
|
+
stackingContextId: resolveCssStackingContextId(el),
|
|
297
309
|
timingSource: "authored",
|
|
298
310
|
zIndex: readTimelineElementZIndex(el),
|
|
299
311
|
};
|
|
@@ -10,6 +10,31 @@
|
|
|
10
10
|
import type { TimelineElement } from "../store/playerStore";
|
|
11
11
|
import type { ClipManifestClip } from "./playbackTypes";
|
|
12
12
|
import { isFinitePositive } from "./playbackAdapter";
|
|
13
|
+
import { getSourceScopedSelectorIndex } from "../../utils/sourceScopedSelectorIndex";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Layer-reveal lift transparency
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Attributes carrying the pre-lift state of a Layers-panel selection reveal
|
|
21
|
+
* (useLayerRevealOverride): while a layer is selected it PAINTS on top via a
|
|
22
|
+
* temporary inline z-index, but the lift is a purely visual, ephemeral studio
|
|
23
|
+
* affordance — every z reader must keep reporting the element's TRUE z
|
|
24
|
+
* (stored here) so menus, badges, the lane mirror, and the panel sort never
|
|
25
|
+
* reason on the lifted value. A z-reorder commit removes the attributes (the
|
|
26
|
+
* commit is the new truth).
|
|
27
|
+
*/
|
|
28
|
+
export const LAYER_REVEAL_PRIOR_Z_ATTR = "data-hf-reveal-prior-z";
|
|
29
|
+
export const LAYER_REVEAL_PRIOR_POSITION_ATTR = "data-hf-reveal-prior-pos";
|
|
30
|
+
|
|
31
|
+
/** The lifted element's true (pre-lift) z, or null when no lift is active. */
|
|
32
|
+
export function readLayerRevealPriorZ(el: Element): number | null {
|
|
33
|
+
const raw = el.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);
|
|
34
|
+
if (raw == null) return null;
|
|
35
|
+
const n = Number.parseInt(raw, 10);
|
|
36
|
+
return Number.isFinite(n) ? n : null;
|
|
37
|
+
}
|
|
13
38
|
|
|
14
39
|
// ---------------------------------------------------------------------------
|
|
15
40
|
// Duration attribute helpers
|
|
@@ -22,8 +47,11 @@ import { isFinitePositive } from "./playbackAdapter";
|
|
|
22
47
|
* back to computed style; "auto" / empty / unparseable ⇒ 0. Works with a
|
|
23
48
|
* detached parse Document (no defaultView) as well as a live iframe. Mirrors
|
|
24
49
|
* canvasContextMenuZOrder.parseZIndex semantics so the two directions agree.
|
|
50
|
+
* Reveal-lift transparent: an active lift reports the stored TRUE z.
|
|
25
51
|
*/
|
|
26
52
|
export function readTimelineElementZIndex(el: Element): number {
|
|
53
|
+
const prior = readLayerRevealPriorZ(el);
|
|
54
|
+
if (prior != null) return prior;
|
|
27
55
|
const html = el as HTMLElement;
|
|
28
56
|
const parseZ = (value: string | null | undefined): number | null => {
|
|
29
57
|
if (value == null || value === "" || value === "auto") return null;
|
|
@@ -237,17 +265,13 @@ export function getTimelineElementSelectorIndex(
|
|
|
237
265
|
el: Element,
|
|
238
266
|
selector: string | undefined,
|
|
239
267
|
): number | undefined {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return matchIndex >= 0 ? matchIndex : undefined;
|
|
248
|
-
} catch {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
268
|
+
return getSourceScopedSelectorIndex(
|
|
269
|
+
doc,
|
|
270
|
+
el,
|
|
271
|
+
selector,
|
|
272
|
+
getTimelineElementSourceFile(el),
|
|
273
|
+
getTimelineElementSourceFile,
|
|
274
|
+
);
|
|
251
275
|
}
|
|
252
276
|
|
|
253
277
|
export function buildTimelineElementKey(params: {
|
|
@@ -298,6 +322,25 @@ export function getTimelineElementIdentity(element: { key?: string | null; id: s
|
|
|
298
322
|
return element.key ?? element.id;
|
|
299
323
|
}
|
|
300
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Timeline store key for a z-reorder entry built OUTSIDE the timeline
|
|
327
|
+
* expansion (canvas context menu / LayersPanel), so the reorder commit can
|
|
328
|
+
* update the store's zIndex synchronously. Matches buildTimelineElementKey's
|
|
329
|
+
* stable branches (`sourceFile#domId`, else the selector-based key). Undefined
|
|
330
|
+
* when the element has neither a DOM id nor a selector — the timeline's
|
|
331
|
+
* fallback branch needs its own fallbackIndex, which these callers don't have,
|
|
332
|
+
* so such an entry simply skips the synchronous store update.
|
|
333
|
+
*/
|
|
334
|
+
export function deriveTimelineStoreKey(params: {
|
|
335
|
+
domId?: string;
|
|
336
|
+
selector?: string;
|
|
337
|
+
selectorIndex?: number;
|
|
338
|
+
sourceFile?: string;
|
|
339
|
+
}): string | undefined {
|
|
340
|
+
if (!params.domId && !params.selector) return undefined;
|
|
341
|
+
return buildTimelineElementKey({ id: "", fallbackIndex: 0, ...params });
|
|
342
|
+
}
|
|
343
|
+
|
|
301
344
|
// ---------------------------------------------------------------------------
|
|
302
345
|
// DOM node querying
|
|
303
346
|
// ---------------------------------------------------------------------------
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
buildTimelineElementIdentity,
|
|
21
21
|
readTimelineElementZIndex,
|
|
22
22
|
} from "./timelineElementHelpers";
|
|
23
|
+
import { postRuntimeControlMessage } from "./runtimeProtocol";
|
|
23
24
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Viewport / DOM normalisation
|
|
@@ -103,10 +104,7 @@ function postPreviewControl(
|
|
|
103
104
|
action: string,
|
|
104
105
|
payload: Record<string, unknown>,
|
|
105
106
|
): void {
|
|
106
|
-
iframe.contentWindow
|
|
107
|
-
{ source: "hf-parent", type: "control", action, ...payload },
|
|
108
|
-
"*",
|
|
109
|
-
);
|
|
107
|
+
postRuntimeControlMessage(iframe.contentWindow, action, payload);
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
export function shouldMutePreviewAudio(audioMuted: boolean, playbackRate: number): boolean {
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
2
|
import { usePlayerStore, liveTime, type TimelineElement } from "./playerStore";
|
|
3
3
|
|
|
4
|
+
/** The playback/selection state `reset()` restores (persistent prefs asserted separately). */
|
|
5
|
+
function expectResettableDefaults(state: ReturnType<typeof usePlayerStore.getState>): void {
|
|
6
|
+
expect(state.isPlaying).toBe(false);
|
|
7
|
+
expect(state.currentTime).toBe(0);
|
|
8
|
+
expect(state.duration).toBe(0);
|
|
9
|
+
expect(state.timelineReady).toBe(false);
|
|
10
|
+
expect(state.elements).toEqual([]);
|
|
11
|
+
expect(state.selectedElementId).toBeNull();
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
describe("usePlayerStore", () => {
|
|
5
15
|
beforeEach(() => {
|
|
6
16
|
usePlayerStore.getState().reset();
|
|
@@ -9,12 +19,7 @@ describe("usePlayerStore", () => {
|
|
|
9
19
|
describe("initial state", () => {
|
|
10
20
|
it("has correct defaults", () => {
|
|
11
21
|
const state = usePlayerStore.getState();
|
|
12
|
-
|
|
13
|
-
expect(state.currentTime).toBe(0);
|
|
14
|
-
expect(state.duration).toBe(0);
|
|
15
|
-
expect(state.timelineReady).toBe(false);
|
|
16
|
-
expect(state.elements).toEqual([]);
|
|
17
|
-
expect(state.selectedElementId).toBeNull();
|
|
22
|
+
expectResettableDefaults(state);
|
|
18
23
|
expect(state.playbackRate).toBe(1);
|
|
19
24
|
expect(state.audioMuted).toBe(false);
|
|
20
25
|
expect(state.loopEnabled).toBe(false);
|
|
@@ -384,6 +389,32 @@ describe("usePlayerStore", () => {
|
|
|
384
389
|
});
|
|
385
390
|
});
|
|
386
391
|
|
|
392
|
+
describe("clipRevealRequest", () => {
|
|
393
|
+
it("starts null and carries the requested element id", () => {
|
|
394
|
+
expect(usePlayerStore.getState().clipRevealRequest).toBeNull();
|
|
395
|
+
usePlayerStore.getState().requestClipReveal("el-1");
|
|
396
|
+
expect(usePlayerStore.getState().clipRevealRequest?.elementId).toBe("el-1");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("bumps the nonce on repeat requests for the same clip", () => {
|
|
400
|
+
usePlayerStore.getState().requestClipReveal("el-1");
|
|
401
|
+
const first = usePlayerStore.getState().clipRevealRequest;
|
|
402
|
+
usePlayerStore.getState().requestClipReveal("el-1");
|
|
403
|
+
const second = usePlayerStore.getState().clipRevealRequest;
|
|
404
|
+
expect(second?.nonce).not.toBe(first?.nonce);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("clears via clearClipRevealRequest and on reset", () => {
|
|
408
|
+
usePlayerStore.getState().requestClipReveal("el-1");
|
|
409
|
+
usePlayerStore.getState().clearClipRevealRequest();
|
|
410
|
+
expect(usePlayerStore.getState().clipRevealRequest).toBeNull();
|
|
411
|
+
|
|
412
|
+
usePlayerStore.getState().requestClipReveal("el-2");
|
|
413
|
+
usePlayerStore.getState().reset();
|
|
414
|
+
expect(usePlayerStore.getState().clipRevealRequest).toBeNull();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
387
418
|
describe("reset", () => {
|
|
388
419
|
it("resets all state to defaults", () => {
|
|
389
420
|
// Mutate everything
|
|
@@ -398,13 +429,7 @@ describe("usePlayerStore", () => {
|
|
|
398
429
|
// Reset
|
|
399
430
|
usePlayerStore.getState().reset();
|
|
400
431
|
|
|
401
|
-
|
|
402
|
-
expect(state.isPlaying).toBe(false);
|
|
403
|
-
expect(state.currentTime).toBe(0);
|
|
404
|
-
expect(state.duration).toBe(0);
|
|
405
|
-
expect(state.timelineReady).toBe(false);
|
|
406
|
-
expect(state.elements).toEqual([]);
|
|
407
|
-
expect(state.selectedElementId).toBeNull();
|
|
432
|
+
expectResettableDefaults(usePlayerStore.getState());
|
|
408
433
|
});
|
|
409
434
|
|
|
410
435
|
it("does not reset playbackRate, audioMuted, loopEnabled, zoomMode, or manualZoomPercent", () => {
|