@hyperframes/studio 0.7.90 → 0.7.93
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-RS9ROX_S.js → hyperframes-player-NSdcoOcK.js} +1 -1
- package/dist/assets/{index-CquRJgm-.js → index-CCGnc_0M.js} +1 -1
- package/dist/assets/index-D78KEjgB.css +1 -0
- package/dist/assets/index-Dp1zVb9X.js +428 -0
- package/dist/assets/{index-Bi-CwQDX.js → index-wS6f7F5q.js} +1 -1
- package/dist/index.d.ts +40 -36
- package/dist/index.html +2 -2
- package/dist/index.js +9598 -6798
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/App.tsx +14 -14
- package/src/components/DesignPanelPromoteProvider.tsx +1 -1
- package/src/components/ExternalFileConflictBanner.test.tsx +142 -0
- package/src/components/ExternalFileConflictBanner.tsx +255 -0
- package/src/components/StudioRightPanel.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +84 -0
- package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
- package/src/components/editor/GsapAnimationSection.tsx +11 -41
- package/src/components/editor/PropertyPanel.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +29 -26
- package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +1 -1
- package/src/components/editor/propertyPanelCommitField.tsx +37 -2
- package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +69 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelTransformCommit.test.ts +57 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +14 -14
- package/src/components/editor/propertyPanelTypes.ts +10 -4
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +65 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +66 -10
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
- package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
- package/src/components/renders/useRenderQueue.ts +16 -3
- package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
- package/src/components/sidebar/AssetCard.tsx +4 -3
- package/src/components/sidebar/AudioRow.tsx +4 -3
- package/src/contexts/DomEditContext.tsx +1 -1
- package/src/contexts/FileManagerContext.tsx +12 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +17 -0
- package/src/hooks/externalFileReloadBus.test.ts +34 -0
- package/src/hooks/externalFileReloadBus.ts +18 -0
- package/src/hooks/gsapDragCommit.test.ts +39 -1
- package/src/hooks/gsapDragCommit.ts +12 -40
- package/src/hooks/gsapEditOutcome.ts +72 -0
- package/src/hooks/gsapResizeIntercept.test.ts +90 -2
- package/src/hooks/gsapResizeIntercept.ts +44 -16
- package/src/hooks/gsapRuntimeBridge.test.ts +195 -27
- package/src/hooks/gsapRuntimeBridge.ts +93 -28
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +194 -24
- package/src/hooks/useAnimatedPropertyCommit.ts +101 -49
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +215 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +61 -0
- package/src/hooks/useDomGeometryCommits.ts +20 -0
- package/src/hooks/useEditorSave.test.tsx +131 -0
- package/src/hooks/useEditorSave.ts +113 -39
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +221 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +429 -0
- package/src/hooks/useFileManager.projectOwnership.test.tsx +139 -7
- package/src/hooks/useFileManager.ts +32 -3
- package/src/hooks/useGsapAnimationFetchFallback.ts +45 -24
- package/src/hooks/useGsapAwareEditing.test.tsx +125 -39
- package/src/hooks/useGsapAwareEditing.ts +88 -27
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +42 -0
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
- package/src/hooks/useGsapKeyframeOps.ts +20 -9
- package/src/hooks/usePreviewPersistence.test.ts +23 -0
- package/src/hooks/usePreviewPersistence.ts +22 -58
- package/src/hooks/useSdkSession.ts +8 -52
- package/src/hooks/useStudioExternalFileChanges.ts +81 -0
- package/src/hooks/useStudioSdkSessions.ts +2 -7
- package/src/hooks/useStudioSessionStart.ts +15 -0
- package/src/hooks/useStudioTestHooks.test.tsx +11 -7
- package/src/player/components/BeatStrip.test.tsx +374 -0
- package/src/player/components/BeatStrip.tsx +295 -37
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
- package/src/player/components/LayerDisclosureRow.tsx +2 -0
- package/src/player/components/Timeline.test.ts +26 -8
- package/src/player/components/Timeline.tsx +87 -88
- package/src/player/components/Timeline.virtualization.test.tsx +54 -11
- package/src/player/components/TimelineCanvas.tsx +14 -71
- package/src/player/components/TimelineClip.test.tsx +17 -2
- package/src/player/components/TimelineClip.tsx +20 -5
- package/src/player/components/TimelineClipDiamonds.test.tsx +536 -27
- package/src/player/components/TimelineClipDiamonds.tsx +118 -223
- package/src/player/components/TimelineCompactDiamonds.tsx +84 -0
- package/src/player/components/TimelineDiamondConnectors.tsx +31 -3
- package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
- package/src/player/components/TimelineGestureOverlay.tsx +96 -0
- package/src/player/components/TimelineLanes.test.tsx +64 -7
- package/src/player/components/TimelineLanes.tsx +84 -66
- package/src/player/components/TimelineOverlays.test.ts +161 -0
- package/src/player/components/TimelineOverlays.tsx +104 -11
- package/src/player/components/TimelinePropertyLanes.test.tsx +9 -5
- package/src/player/components/TimelinePropertyLanes.tsx +9 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -0
- package/src/player/components/TimelineTrackHeader.tsx +16 -0
- package/src/player/components/TimelineTrackRow.tsx +58 -7
- package/src/player/components/timelineCallbacks.ts +1 -1
- package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
- package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
- package/src/player/components/timelineClipDragPreview.test.ts +33 -0
- package/src/player/components/timelineClipDragPreview.ts +18 -12
- package/src/player/components/timelineClipDragTypes.ts +12 -0
- package/src/player/components/timelineDiamondTypes.ts +18 -21
- package/src/player/components/timelineDragDrop.test.tsx +206 -0
- package/src/player/components/timelineDragDrop.ts +152 -30
- package/src/player/components/timelineKeyboardNavigation.test.ts +303 -0
- package/src/player/components/timelineKeyboardNavigation.ts +337 -0
- package/src/player/components/timelineLaneProps.ts +3 -1
- package/src/player/components/timelineMarquee.test.ts +73 -25
- package/src/player/components/timelineMarquee.ts +44 -16
- package/src/player/components/timelineNavigationIdentity.ts +38 -0
- package/src/player/components/timelineTestViewport.ts +13 -0
- package/src/player/components/timelineViewModel.ts +4 -4
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
- package/src/player/components/useTimelineClipDrag.ts +112 -70
- package/src/player/components/useTimelineClipRenderWindow.ts +8 -39
- package/src/player/components/useTimelineFocusCoordinator.test.tsx +174 -0
- package/src/player/components/useTimelineFocusCoordinator.ts +221 -0
- package/src/player/components/useTimelineKeyboardActor.test.tsx +267 -0
- package/src/player/components/useTimelineKeyboardActor.ts +147 -0
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
- package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
- package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
- package/src/player/components/useTimelineLogicalFocus.ts +80 -0
- package/src/player/components/useTimelineLogicalRows.test.tsx +57 -0
- package/src/player/components/useTimelineLogicalRows.ts +47 -0
- package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
- package/src/player/components/useTimelineRangeSelection.ts +202 -69
- package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
- package/src/player/components/useTimelineRowVirtualization.ts +26 -29
- package/src/player/components/useTrackGapMenu.test.tsx +64 -0
- package/src/player/components/useTrackGapMenu.ts +32 -9
- package/src/player/store/keyframeSlice.ts +59 -18
- package/src/player/store/liveTime.ts +11 -0
- package/src/player/store/playerStore.test.ts +106 -23
- package/src/player/store/playerStore.ts +43 -40
- package/src/player/store/timelineFocusState.ts +15 -0
- package/src/telemetry/canary.test.ts +325 -0
- package/src/telemetry/canary.ts +268 -0
- package/src/telemetry/client.test.ts +8 -3
- package/src/telemetry/client.ts +13 -43
- package/src/telemetry/config.ts +16 -2
- package/src/telemetry/distinctId.test.ts +35 -0
- package/src/telemetry/distinctId.ts +11 -2
- package/src/telemetry/policy.test.ts +81 -0
- package/src/telemetry/policy.ts +104 -0
- package/src/utils/domEditSaveQueue.test.ts +37 -1
- package/src/utils/domEditSaveQueue.ts +22 -4
- package/src/utils/externalConflictStorage.test.ts +95 -0
- package/src/utils/externalConflictStorage.ts +172 -0
- package/src/utils/studioFileVersion.test.ts +28 -1
- package/src/utils/studioFileVersion.ts +30 -0
- package/src/utils/studioPendingEdits.test.ts +108 -1
- package/src/utils/studioPendingEdits.ts +39 -2
- package/src/utils/studioSaveDiagnostics.ts +5 -0
- package/src/utils/studioTelemetry.test.ts +76 -0
- package/src/utils/studioTelemetry.ts +14 -6
- package/dist/assets/index-BXkzp_OU.css +0 -1
- package/dist/assets/index-Cql15Yur.js +0 -428
- package/src/player/components/useTimelineRevealClip.test.tsx +0 -139
- package/src/player/components/useTimelineRevealClip.ts +0 -180
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
|
|
3
|
-
import React, { act, useRef } from "react";
|
|
4
|
-
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
-
import type { TimelineElement } from "../store/playerStore";
|
|
7
|
-
import { usePlayerStore } from "../store/playerStore";
|
|
8
|
-
import { createTimelineRowGeometry } from "./timelineLayout";
|
|
9
|
-
import { useTimelineRevealClip } from "./useTimelineRevealClip";
|
|
10
|
-
|
|
11
|
-
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
12
|
-
|
|
13
|
-
const element: TimelineElement = {
|
|
14
|
-
id: "hero",
|
|
15
|
-
tag: "div",
|
|
16
|
-
start: 20,
|
|
17
|
-
duration: 2,
|
|
18
|
-
track: 1,
|
|
19
|
-
};
|
|
20
|
-
const geometry = createTimelineRowGeometry([1], [48]);
|
|
21
|
-
|
|
22
|
-
function createHarnessRoot() {
|
|
23
|
-
const host = document.createElement("div");
|
|
24
|
-
document.body.append(host);
|
|
25
|
-
return { host, root: createRoot(host) };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface HarnessProps {
|
|
29
|
-
mounted: boolean;
|
|
30
|
-
version: number;
|
|
31
|
-
width?: number;
|
|
32
|
-
height?: number;
|
|
33
|
-
deferFocusUntilViewportUpdate?: boolean;
|
|
34
|
-
focusedElementId?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function Harness({
|
|
38
|
-
mounted,
|
|
39
|
-
version,
|
|
40
|
-
width = 300,
|
|
41
|
-
height = 100,
|
|
42
|
-
deferFocusUntilViewportUpdate = false,
|
|
43
|
-
focusedElementId,
|
|
44
|
-
}: HarnessProps) {
|
|
45
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
46
|
-
useTimelineRevealClip({
|
|
47
|
-
scrollRef,
|
|
48
|
-
elements: [element],
|
|
49
|
-
rowGeometry: geometry,
|
|
50
|
-
pixelsPerSecond: 100,
|
|
51
|
-
contentOrigin: 32,
|
|
52
|
-
allowHorizontal: true,
|
|
53
|
-
deferFocusUntilViewportUpdate,
|
|
54
|
-
focusedElementId,
|
|
55
|
-
viewportVersion: version,
|
|
56
|
-
sessionEpoch: 1,
|
|
57
|
-
});
|
|
58
|
-
return (
|
|
59
|
-
<div
|
|
60
|
-
ref={(node) => {
|
|
61
|
-
scrollRef.current = node;
|
|
62
|
-
if (node) {
|
|
63
|
-
Object.defineProperty(node, "clientWidth", { configurable: true, value: width });
|
|
64
|
-
Object.defineProperty(node, "clientHeight", { configurable: true, value: height });
|
|
65
|
-
}
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
{mounted && <div data-el-id="hero" tabIndex={-1} />}
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function renderHarness(root: Root, props: HarnessProps): Promise<void> {
|
|
74
|
-
await act(async () => root.render(<Harness {...props} />));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
afterEach(() => {
|
|
78
|
-
usePlayerStore.getState().reset();
|
|
79
|
-
document.body.replaceChildren();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("useTimelineRevealClip", () => {
|
|
83
|
-
it("scrolls from model coordinates, then consumes only after the clip mounts", async () => {
|
|
84
|
-
const { host, root } = createHarnessRoot();
|
|
85
|
-
usePlayerStore.getState().requestClipReveal("hero");
|
|
86
|
-
|
|
87
|
-
await renderHarness(root, { mounted: false, version: 0 });
|
|
88
|
-
const scroll = host.firstElementChild as HTMLDivElement;
|
|
89
|
-
expect(scroll.scrollLeft).toBe(1_944);
|
|
90
|
-
expect(usePlayerStore.getState().clipRevealRequest?.elementId).toBe("hero");
|
|
91
|
-
|
|
92
|
-
await renderHarness(root, { mounted: true, version: 1 });
|
|
93
|
-
expect(usePlayerStore.getState().clipRevealRequest).toBeNull();
|
|
94
|
-
expect(document.activeElement?.getAttribute("data-el-id")).toBe("hero");
|
|
95
|
-
|
|
96
|
-
scroll.scrollLeft = 0;
|
|
97
|
-
await act(async () => usePlayerStore.getState().requestClipReveal("hero"));
|
|
98
|
-
await renderHarness(root, { mounted: true, version: 2 });
|
|
99
|
-
expect(scroll.scrollLeft).toBe(1_944);
|
|
100
|
-
expect(usePlayerStore.getState().clipRevealRequest).toBeNull();
|
|
101
|
-
await act(async () => root.unmount());
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("consumes an invalid target without scrolling", async () => {
|
|
105
|
-
const { root } = createHarnessRoot();
|
|
106
|
-
usePlayerStore.getState().requestClipReveal("missing");
|
|
107
|
-
await renderHarness(root, { mounted: false, version: 0 });
|
|
108
|
-
expect(usePlayerStore.getState().clipRevealRequest).toBeNull();
|
|
109
|
-
await act(async () => root.unmount());
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("keeps a reveal pending through zero-size viewport and virtualized focus handoff", async () => {
|
|
113
|
-
const { host, root } = createHarnessRoot();
|
|
114
|
-
usePlayerStore.getState().requestClipReveal("hero");
|
|
115
|
-
|
|
116
|
-
await renderHarness(root, { mounted: true, version: 0, width: 0, height: 0 });
|
|
117
|
-
const scroll = host.firstElementChild as HTMLDivElement;
|
|
118
|
-
expect(scroll.scrollLeft).toBe(0);
|
|
119
|
-
expect(usePlayerStore.getState().clipRevealRequest?.elementId).toBe("hero");
|
|
120
|
-
expect(document.activeElement?.getAttribute("data-el-id")).not.toBe("hero");
|
|
121
|
-
|
|
122
|
-
await renderHarness(root, {
|
|
123
|
-
mounted: true,
|
|
124
|
-
version: 1,
|
|
125
|
-
deferFocusUntilViewportUpdate: true,
|
|
126
|
-
});
|
|
127
|
-
expect(scroll.scrollLeft).toBe(1_944);
|
|
128
|
-
expect(usePlayerStore.getState().clipRevealRequest?.elementId).toBe("hero");
|
|
129
|
-
await renderHarness(root, {
|
|
130
|
-
mounted: true,
|
|
131
|
-
version: 2,
|
|
132
|
-
deferFocusUntilViewportUpdate: true,
|
|
133
|
-
focusedElementId: "hero",
|
|
134
|
-
});
|
|
135
|
-
expect(usePlayerStore.getState().clipRevealRequest).toBeNull();
|
|
136
|
-
expect(document.activeElement?.getAttribute("data-el-id")).toBe("hero");
|
|
137
|
-
await act(async () => root.unmount());
|
|
138
|
-
});
|
|
139
|
-
});
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
2
|
-
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
-
import { usePlayerStore } from "../store/playerStore";
|
|
4
|
-
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
|
5
|
-
import { CLIP_Y, RULER_H, type TimelineRowGeometry } from "./timelineLayout";
|
|
6
|
-
import { computeRevealScroll } from "./timelineRevealScroll";
|
|
7
|
-
|
|
8
|
-
interface UseTimelineRevealClipInput {
|
|
9
|
-
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
10
|
-
elements: readonly TimelineElement[];
|
|
11
|
-
rowGeometry: TimelineRowGeometry;
|
|
12
|
-
pixelsPerSecond: number;
|
|
13
|
-
contentOrigin: number;
|
|
14
|
-
allowHorizontal: boolean;
|
|
15
|
-
deferFocusUntilViewportUpdate: boolean;
|
|
16
|
-
focusedElementId?: string;
|
|
17
|
-
viewportVersion: unknown;
|
|
18
|
-
sessionEpoch: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function escapeSelectorValue(value: string): string {
|
|
22
|
-
return typeof CSS !== "undefined" && typeof CSS.escape === "function"
|
|
23
|
-
? CSS.escape(value)
|
|
24
|
-
: value.replace(/["\\]/g, "\\$&");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function scrollToTimelineElement(
|
|
28
|
-
container: HTMLDivElement,
|
|
29
|
-
element: TimelineElement,
|
|
30
|
-
row: number,
|
|
31
|
-
rowGeometry: TimelineRowGeometry,
|
|
32
|
-
pixelsPerSecond: number,
|
|
33
|
-
contentOrigin: number,
|
|
34
|
-
allowHorizontal: boolean,
|
|
35
|
-
): boolean {
|
|
36
|
-
const clipLeft = contentOrigin + element.start * pixelsPerSecond;
|
|
37
|
-
const target = computeRevealScroll({
|
|
38
|
-
scrollLeft: container.scrollLeft,
|
|
39
|
-
scrollTop: container.scrollTop,
|
|
40
|
-
viewportWidth: container.clientWidth,
|
|
41
|
-
viewportHeight: container.clientHeight,
|
|
42
|
-
clipLeft,
|
|
43
|
-
clipRight: clipLeft + Math.max(element.duration * pixelsPerSecond, 4),
|
|
44
|
-
clipTop: rowGeometry.getRowTop(row) + CLIP_Y,
|
|
45
|
-
clipBottom: rowGeometry.getRowTop(row) + rowGeometry.getRowHeight(row) - CLIP_Y,
|
|
46
|
-
stickyLeft: contentOrigin,
|
|
47
|
-
stickyTop: RULER_H,
|
|
48
|
-
allowHorizontal,
|
|
49
|
-
});
|
|
50
|
-
if (target.left !== null) container.scrollLeft = target.left;
|
|
51
|
-
if (target.top !== null) container.scrollTop = target.top;
|
|
52
|
-
const didScroll = target.left !== null || target.top !== null;
|
|
53
|
-
if (didScroll) container.dispatchEvent(new Event("scroll"));
|
|
54
|
-
return didScroll;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function focusRevealedElement(container: HTMLDivElement, elementId: string): boolean {
|
|
58
|
-
const clip = container.querySelector(`[data-el-id="${escapeSelectorValue(elementId)}"]`);
|
|
59
|
-
if (!(clip instanceof HTMLElement)) return false;
|
|
60
|
-
const alreadyHighlighted = clip.hasAttribute("data-reveal-highlight");
|
|
61
|
-
clip.setAttribute("data-reveal-highlight", "true");
|
|
62
|
-
clip.focus({ preventScroll: true });
|
|
63
|
-
if (document.activeElement !== clip) {
|
|
64
|
-
clip.removeAttribute("data-reveal-highlight");
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
if (!alreadyHighlighted) {
|
|
68
|
-
clip.addEventListener("blur", () => clip.removeAttribute("data-reveal-highlight"), {
|
|
69
|
-
once: true,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function focusAndConsumeReveal(
|
|
76
|
-
container: HTMLDivElement,
|
|
77
|
-
request: { elementId: string; nonce: number },
|
|
78
|
-
deferUntilFocusPin: boolean,
|
|
79
|
-
focusedElementId?: string,
|
|
80
|
-
): void {
|
|
81
|
-
if (!focusRevealedElement(container, request.elementId)) return;
|
|
82
|
-
if (deferUntilFocusPin && focusedElementId !== request.elementId) return;
|
|
83
|
-
if (usePlayerStore.getState().clipRevealRequest === request) {
|
|
84
|
-
usePlayerStore.getState().clearClipRevealRequest();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function resolveRevealTarget(
|
|
89
|
-
elements: readonly TimelineElement[],
|
|
90
|
-
rowGeometry: TimelineRowGeometry,
|
|
91
|
-
elementId: string,
|
|
92
|
-
): { element: TimelineElement; row: number } | null {
|
|
93
|
-
const element = elements.find((candidate) => getTimelineElementIdentity(candidate) === elementId);
|
|
94
|
-
if (!element) return null;
|
|
95
|
-
const row = rowGeometry.getRowIndex(element.track);
|
|
96
|
-
return row < 0 ? null : { element, row };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function shouldScrollReveal(
|
|
100
|
-
previous: { request: { elementId: string; nonce: number }; sessionEpoch: number } | null,
|
|
101
|
-
request: { elementId: string; nonce: number },
|
|
102
|
-
sessionEpoch: number,
|
|
103
|
-
): boolean {
|
|
104
|
-
return previous?.request !== request || previous.sessionEpoch !== sessionEpoch;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Coordinate-first reveal; the request remains pinned until its clip mounts. */
|
|
108
|
-
export function useTimelineRevealClip({
|
|
109
|
-
scrollRef,
|
|
110
|
-
elements,
|
|
111
|
-
rowGeometry,
|
|
112
|
-
pixelsPerSecond,
|
|
113
|
-
contentOrigin,
|
|
114
|
-
allowHorizontal,
|
|
115
|
-
deferFocusUntilViewportUpdate,
|
|
116
|
-
focusedElementId,
|
|
117
|
-
viewportVersion,
|
|
118
|
-
sessionEpoch,
|
|
119
|
-
}: UseTimelineRevealClipInput): void {
|
|
120
|
-
const revealRequest = usePlayerStore((state) => state.clipRevealRequest);
|
|
121
|
-
const scrolledRequestRef = useRef<{
|
|
122
|
-
request: { elementId: string; nonce: number };
|
|
123
|
-
sessionEpoch: number;
|
|
124
|
-
} | null>(null);
|
|
125
|
-
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
if (!revealRequest) {
|
|
128
|
-
scrolledRequestRef.current = null;
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const target = resolveRevealTarget(elements, rowGeometry, revealRequest.elementId);
|
|
132
|
-
if (!target) {
|
|
133
|
-
usePlayerStore.getState().clearClipRevealRequest();
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const container = scrollRef.current;
|
|
137
|
-
if (!container) return;
|
|
138
|
-
if (container.clientWidth <= 0 || container.clientHeight <= 0) return;
|
|
139
|
-
|
|
140
|
-
if (shouldScrollReveal(scrolledRequestRef.current, revealRequest, sessionEpoch)) {
|
|
141
|
-
scrolledRequestRef.current = { request: revealRequest, sessionEpoch };
|
|
142
|
-
const didScroll = scrollToTimelineElement(
|
|
143
|
-
container,
|
|
144
|
-
target.element,
|
|
145
|
-
target.row,
|
|
146
|
-
rowGeometry,
|
|
147
|
-
pixelsPerSecond,
|
|
148
|
-
contentOrigin,
|
|
149
|
-
allowHorizontal,
|
|
150
|
-
);
|
|
151
|
-
// Keep the reveal pin alive until the scroll snapshot catches up. If the
|
|
152
|
-
// request were consumed here, horizontal windowing could unmount and
|
|
153
|
-
// recreate the focused clip between the programmatic scroll and the next
|
|
154
|
-
// viewport publication.
|
|
155
|
-
if (didScroll && deferFocusUntilViewportUpdate) return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Focus is the durable row/clip pin. Do not consume the reveal pin until
|
|
159
|
-
// the focus listener has published that replacement, or windowing can
|
|
160
|
-
// briefly unmount and recreate the element between the two owners.
|
|
161
|
-
focusAndConsumeReveal(
|
|
162
|
-
container,
|
|
163
|
-
revealRequest,
|
|
164
|
-
deferFocusUntilViewportUpdate,
|
|
165
|
-
focusedElementId,
|
|
166
|
-
);
|
|
167
|
-
}, [
|
|
168
|
-
allowHorizontal,
|
|
169
|
-
contentOrigin,
|
|
170
|
-
deferFocusUntilViewportUpdate,
|
|
171
|
-
elements,
|
|
172
|
-
focusedElementId,
|
|
173
|
-
pixelsPerSecond,
|
|
174
|
-
revealRequest,
|
|
175
|
-
rowGeometry,
|
|
176
|
-
scrollRef,
|
|
177
|
-
sessionEpoch,
|
|
178
|
-
viewportVersion,
|
|
179
|
-
]);
|
|
180
|
-
}
|