@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { defaultTimelineTheme } from "./timelineTheme";
|
|
7
|
+
import { TimelineGestureOverlay } from "./TimelineGestureOverlay";
|
|
8
|
+
import type { DraggedClipState } from "./timelineClipDragTypes";
|
|
9
|
+
import { getTrackStyle } from "./useTimelineTrackLayout";
|
|
10
|
+
|
|
11
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
12
|
+
|
|
13
|
+
const drag: DraggedClipState = {
|
|
14
|
+
pointerId: 0,
|
|
15
|
+
element: { id: "hero", tag: "div", start: 2, duration: 3, track: 1 },
|
|
16
|
+
originClientX: 100,
|
|
17
|
+
originClientY: 100,
|
|
18
|
+
originScrollLeft: 0,
|
|
19
|
+
originScrollTop: 0,
|
|
20
|
+
pointerClientX: 350,
|
|
21
|
+
pointerClientY: 240,
|
|
22
|
+
pointerOffsetX: 20,
|
|
23
|
+
pointerOffsetY: 10,
|
|
24
|
+
previewStart: 4,
|
|
25
|
+
previewTrack: 2,
|
|
26
|
+
insertRow: null,
|
|
27
|
+
snapTime: null,
|
|
28
|
+
snapType: null,
|
|
29
|
+
started: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
afterEach(() => document.body.replaceChildren());
|
|
33
|
+
|
|
34
|
+
describe("TimelineGestureOverlay", () => {
|
|
35
|
+
it("keeps the drag actor mounted without a source-row node", () => {
|
|
36
|
+
const host = document.createElement("div");
|
|
37
|
+
document.body.append(host);
|
|
38
|
+
const root = createRoot(host);
|
|
39
|
+
const scroll = {
|
|
40
|
+
scrollLeft: 500,
|
|
41
|
+
scrollTop: 300,
|
|
42
|
+
getBoundingClientRect: () => ({ left: 50, top: 40 }),
|
|
43
|
+
} as HTMLDivElement;
|
|
44
|
+
act(() => {
|
|
45
|
+
root.render(
|
|
46
|
+
<TimelineGestureOverlay
|
|
47
|
+
drag={drag}
|
|
48
|
+
scrollRef={{ current: scroll }}
|
|
49
|
+
pixelsPerSecond={100}
|
|
50
|
+
rowHeight={42}
|
|
51
|
+
selectedElementId="hero"
|
|
52
|
+
currentTime={4}
|
|
53
|
+
theme={defaultTimelineTheme}
|
|
54
|
+
getTrackStyle={getTrackStyle}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
const actor = host.querySelector<HTMLElement>('[data-timeline-gesture-actor="hero"]');
|
|
59
|
+
expect(actor?.style.left).toBe("780px");
|
|
60
|
+
expect(actor?.style.top).toBe("490px");
|
|
61
|
+
expect(actor?.querySelector(".timeline-clip")).not.toBeNull();
|
|
62
|
+
expect(actor?.querySelector("[data-el-id]")).toBeNull();
|
|
63
|
+
expect(actor?.querySelector("[data-clip]")).toBeNull();
|
|
64
|
+
expect(host.querySelector("[data-source-row]")).toBeNull();
|
|
65
|
+
act(() => root.unmount());
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps the stable overlay host after terminal cleanup", () => {
|
|
69
|
+
const host = document.createElement("div");
|
|
70
|
+
document.body.append(host);
|
|
71
|
+
const root = createRoot(host);
|
|
72
|
+
act(() => {
|
|
73
|
+
root.render(
|
|
74
|
+
<TimelineGestureOverlay
|
|
75
|
+
drag={null}
|
|
76
|
+
scrollRef={{ current: null }}
|
|
77
|
+
pixelsPerSecond={100}
|
|
78
|
+
rowHeight={42}
|
|
79
|
+
selectedElementId={null}
|
|
80
|
+
currentTime={0}
|
|
81
|
+
theme={defaultTimelineTheme}
|
|
82
|
+
getTrackStyle={getTrackStyle}
|
|
83
|
+
/>,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
expect(host.querySelector("[data-timeline-gesture-overlay]")).not.toBeNull();
|
|
87
|
+
expect(host.querySelector("[data-timeline-gesture-actor]")).toBeNull();
|
|
88
|
+
act(() => root.unmount());
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { memo, type ReactNode } from "react";
|
|
2
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
+
import type { TimelineTheme } from "./timelineTheme";
|
|
4
|
+
import { getRenderedTimelineElement } from "./timelineTheme";
|
|
5
|
+
import { TimelineClip } from "./TimelineClip";
|
|
6
|
+
import { getTimelineEditCapabilities } from "./timelineEditing";
|
|
7
|
+
import { renderClipChildren } from "./timelineClipChildren";
|
|
8
|
+
import { getTimelineDragOverlayPosition } from "./timelineClipDragPreview";
|
|
9
|
+
import type { DraggedClipState } from "./timelineClipDragTypes";
|
|
10
|
+
import type { TrackVisualStyle } from "./timelineIcons";
|
|
11
|
+
import { isTimelineClipActive } from "./useTimelineActiveClips";
|
|
12
|
+
|
|
13
|
+
interface TimelineGestureOverlayProps {
|
|
14
|
+
drag: DraggedClipState | null;
|
|
15
|
+
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
16
|
+
pixelsPerSecond: number;
|
|
17
|
+
rowHeight: number;
|
|
18
|
+
selectedElementId: string | null;
|
|
19
|
+
currentTime: number;
|
|
20
|
+
theme: TimelineTheme;
|
|
21
|
+
getTrackStyle: (tag: string) => TrackVisualStyle;
|
|
22
|
+
renderClipContent?: (
|
|
23
|
+
element: TimelineElement,
|
|
24
|
+
style: { clip: string; label: string },
|
|
25
|
+
) => ReactNode;
|
|
26
|
+
renderClipOverlay?: (element: TimelineElement) => ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Stable canvas child that owns the live drag actor independently of source rows. */
|
|
30
|
+
export const TimelineGestureOverlay = memo(function TimelineGestureOverlay({
|
|
31
|
+
drag,
|
|
32
|
+
scrollRef,
|
|
33
|
+
pixelsPerSecond,
|
|
34
|
+
rowHeight,
|
|
35
|
+
selectedElementId,
|
|
36
|
+
currentTime,
|
|
37
|
+
theme,
|
|
38
|
+
getTrackStyle,
|
|
39
|
+
renderClipContent,
|
|
40
|
+
renderClipOverlay,
|
|
41
|
+
}: TimelineGestureOverlayProps) {
|
|
42
|
+
const element =
|
|
43
|
+
drag?.started === true
|
|
44
|
+
? getRenderedTimelineElement({
|
|
45
|
+
element: drag.element,
|
|
46
|
+
draggedElementId: drag.element.key ?? drag.element.id,
|
|
47
|
+
previewStart: drag.previewStart,
|
|
48
|
+
previewTrack: drag.previewTrack,
|
|
49
|
+
})
|
|
50
|
+
: null;
|
|
51
|
+
const position = drag ? getTimelineDragOverlayPosition(drag, scrollRef.current) : null;
|
|
52
|
+
return (
|
|
53
|
+
<div data-timeline-gesture-overlay className="absolute inset-0 pointer-events-none">
|
|
54
|
+
{element && position && (
|
|
55
|
+
<div
|
|
56
|
+
data-timeline-gesture-actor={element.key ?? element.id}
|
|
57
|
+
className="absolute"
|
|
58
|
+
style={{
|
|
59
|
+
top: position.top,
|
|
60
|
+
left: position.left,
|
|
61
|
+
width: Math.max(element.duration * pixelsPerSecond, 4),
|
|
62
|
+
height: rowHeight,
|
|
63
|
+
zIndex: 40,
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<TimelineClip
|
|
67
|
+
el={{ ...element, start: 0 }}
|
|
68
|
+
pps={pixelsPerSecond}
|
|
69
|
+
clipY={0}
|
|
70
|
+
isSelected={selectedElementId === (element.key ?? element.id)}
|
|
71
|
+
isHovered={false}
|
|
72
|
+
isDragging
|
|
73
|
+
isGestureActor
|
|
74
|
+
isActive={isTimelineClipActive(element, currentTime)}
|
|
75
|
+
hasCustomContent={!!renderClipContent}
|
|
76
|
+
capabilities={getTimelineEditCapabilities(element)}
|
|
77
|
+
theme={theme}
|
|
78
|
+
isComposition={!!element.compositionSrc}
|
|
79
|
+
onHoverStart={() => {}}
|
|
80
|
+
onHoverEnd={() => {}}
|
|
81
|
+
onResizeStart={() => {}}
|
|
82
|
+
onClick={() => {}}
|
|
83
|
+
onDoubleClick={() => {}}
|
|
84
|
+
>
|
|
85
|
+
{renderClipChildren(
|
|
86
|
+
element,
|
|
87
|
+
getTrackStyle(element.tag),
|
|
88
|
+
renderClipContent,
|
|
89
|
+
renderClipOverlay,
|
|
90
|
+
)}
|
|
91
|
+
</TimelineClip>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
});
|
|
@@ -9,6 +9,7 @@ import { getTrackStyle } from "./timelineIcons";
|
|
|
9
9
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
10
10
|
import { TRACK_H, getTimelineRowGeometry } from "./timelineLayout";
|
|
11
11
|
import { createTimelineClipIndex } from "../lib/timelineClipIndex";
|
|
12
|
+
import { buildTimelineLogicalRows } from "./timelineKeyboardNavigation";
|
|
12
13
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
13
14
|
import type { MultiDragPreviewInput } from "./timelineMultiDragPreview";
|
|
14
15
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
@@ -26,6 +27,17 @@ afterEach(() => {
|
|
|
26
27
|
const TRACK_A = 1 / 6;
|
|
27
28
|
const TRACK_B = 0.5;
|
|
28
29
|
|
|
30
|
+
/** Every string a screen reader or a sighted user actually reads. */
|
|
31
|
+
function visibleText(host: HTMLElement): string {
|
|
32
|
+
return host.textContent ?? "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ariaLabels(host: HTMLElement): string {
|
|
36
|
+
return Array.from(host.querySelectorAll("[aria-label]"))
|
|
37
|
+
.map((el) => el.getAttribute("aria-label") ?? "")
|
|
38
|
+
.join(" ");
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
function element(id: string, track: number): TimelineElement {
|
|
30
42
|
return { id, label: id, tag: "div", start: 0, duration: 2, track };
|
|
31
43
|
}
|
|
@@ -94,6 +106,16 @@ function renderLanes(options: RenderLanesOptions = {}): {
|
|
|
94
106
|
rowGeometry={getTimelineRowGeometry(rowHeights)}
|
|
95
107
|
virtualRows={displayTrackOrder.map((_, index) => ({ index, rowKey: index }))}
|
|
96
108
|
rowsVirtualized={false}
|
|
109
|
+
focusedTargetId={null}
|
|
110
|
+
logicalRows={buildTimelineLogicalRows({
|
|
111
|
+
tracks,
|
|
112
|
+
displayTrackOrder,
|
|
113
|
+
laneCounts,
|
|
114
|
+
selectedElementId: null,
|
|
115
|
+
selectedElementIds: next.selectedElementIds ?? new Set(),
|
|
116
|
+
expandedClipIds: new Set(next.expandedClipIds ?? []),
|
|
117
|
+
gsapAnimations,
|
|
118
|
+
})}
|
|
97
119
|
clipIndex={createTimelineClipIndex(tracks)}
|
|
98
120
|
renderTimeRange={{ start: 0, end: Number.POSITIVE_INFINITY }}
|
|
99
121
|
pinnedClipIdentities={new Set()}
|
|
@@ -116,7 +138,6 @@ function renderLanes(options: RenderLanesOptions = {}): {
|
|
|
116
138
|
setResizingClip={vi.fn()}
|
|
117
139
|
setDraggedClip={vi.fn()}
|
|
118
140
|
setSelectedElementId={vi.fn()}
|
|
119
|
-
syncClipDragAutoScroll={vi.fn()}
|
|
120
141
|
shiftClickClipRef={createRef()}
|
|
121
142
|
getPreviewElement={(el) => el}
|
|
122
143
|
getTrackStyle={getTrackStyle}
|
|
@@ -153,7 +174,11 @@ describe("TimelineLanes track numbering", () => {
|
|
|
153
174
|
|
|
154
175
|
expect(visibilityLabels(view.host)).toEqual(["Hide track 1", "Hide track 2"]);
|
|
155
176
|
expect(view.host.querySelectorAll("[data-timeline-row]")).toHaveLength(2);
|
|
156
|
-
|
|
177
|
+
// Only what a user reads. The fractional key still identifies the row in
|
|
178
|
+
// `id` / `data-` attributes, which is exactly where an opaque sort key
|
|
179
|
+
// belongs.
|
|
180
|
+
expect(visibleText(view.host)).not.toContain("0.16666666666666666");
|
|
181
|
+
expect(ariaLabels(view.host)).not.toContain("0.16666666666666666");
|
|
157
182
|
act(() => view.root.unmount());
|
|
158
183
|
});
|
|
159
184
|
|
|
@@ -180,11 +205,12 @@ describe("TimelineLanes track numbering", () => {
|
|
|
180
205
|
onContextMenuLane,
|
|
181
206
|
});
|
|
182
207
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
208
|
+
// The lane's own content cell: the track row's second child, after the
|
|
209
|
+
// sticky header column.
|
|
210
|
+
const secondTrackContent = view.host
|
|
211
|
+
.querySelectorAll("[data-timeline-row]")[1]
|
|
212
|
+
?.querySelector('[role="row"]')
|
|
213
|
+
?.children.item(1);
|
|
188
214
|
act(() => {
|
|
189
215
|
secondTrackContent?.dispatchEvent(
|
|
190
216
|
new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: 100 }),
|
|
@@ -239,9 +265,40 @@ describe("TimelineLanes disclosure target", () => {
|
|
|
239
265
|
);
|
|
240
266
|
const firstIds = idsFor(first.host);
|
|
241
267
|
const secondIds = idsFor(second.host);
|
|
268
|
+
const cellIdsFor = (host: HTMLElement) =>
|
|
269
|
+
new Set(
|
|
270
|
+
Array.from(host.querySelectorAll<HTMLElement>("[data-property-group][id]"), (cell) =>
|
|
271
|
+
cell.getAttribute("id"),
|
|
272
|
+
).filter((id): id is string => id !== null),
|
|
273
|
+
);
|
|
274
|
+
const ownedIdsFor = (host: HTMLElement) =>
|
|
275
|
+
Array.from(host.querySelectorAll("[aria-owns]"), (owner) =>
|
|
276
|
+
owner.getAttribute("aria-owns"),
|
|
277
|
+
).filter((id): id is string => id !== null);
|
|
278
|
+
const firstCellIds = cellIdsFor(first.host);
|
|
279
|
+
const secondCellIds = cellIdsFor(second.host);
|
|
242
280
|
|
|
281
|
+
for (const { host } of [first, second]) {
|
|
282
|
+
const treegrid = host.querySelector<HTMLElement>('[role="treegrid"]');
|
|
283
|
+
expect(treegrid?.getAttribute("aria-colcount")).toBe("2");
|
|
284
|
+
expect(treegrid?.hasAttribute("aria-multiselectable")).toBe(false);
|
|
285
|
+
expect(
|
|
286
|
+
[...host.querySelectorAll('[role="rowheader"]')].every(
|
|
287
|
+
(cell) => cell.getAttribute("aria-colindex") === "1",
|
|
288
|
+
),
|
|
289
|
+
).toBe(true);
|
|
290
|
+
expect(
|
|
291
|
+
[...host.querySelectorAll('[role="gridcell"]')].every(
|
|
292
|
+
(cell) => cell.getAttribute("aria-colindex") === "2",
|
|
293
|
+
),
|
|
294
|
+
).toBe(true);
|
|
295
|
+
}
|
|
243
296
|
expect(firstIds.length).toBeGreaterThan(0);
|
|
244
297
|
expect(firstIds.some((id) => secondIds.includes(id))).toBe(false);
|
|
298
|
+
expect(firstCellIds.size).toBeGreaterThan(0);
|
|
299
|
+
expect([...firstCellIds].some((id) => secondCellIds.has(id))).toBe(false);
|
|
300
|
+
expect(ownedIdsFor(first.host).every((id) => firstCellIds.has(id))).toBe(true);
|
|
301
|
+
expect(ownedIdsFor(second.host).every((id) => secondCellIds.has(id))).toBe(true);
|
|
245
302
|
// Still a legal CSS id selector: the aria-controls lookups above use `#id`.
|
|
246
303
|
for (const id of [...firstIds, ...secondIds]) {
|
|
247
304
|
expect(id).toMatch(/^[A-Za-z][\w-]*$/);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Fragment, useId } from "react";
|
|
1
|
+
import { Fragment, useId, useMemo } from "react";
|
|
2
2
|
import { BeatStrip, BeatBackgroundLines } from "./BeatStrip";
|
|
3
3
|
import { TimelineClip } from "./TimelineClip";
|
|
4
|
-
import {
|
|
4
|
+
import { TimelineCompactDiamonds } from "./TimelineCompactDiamonds";
|
|
5
5
|
import { TimelinePropertyLanes } from "./TimelinePropertyLanes";
|
|
6
6
|
import { TimelineTrackHeader } from "./TimelineTrackHeader";
|
|
7
7
|
import { resolveTrackKeyframeClip } from "./useTimelineTrackLayout";
|
|
@@ -27,6 +27,9 @@ import { TimelineTrackRow } from "./TimelineTrackRow";
|
|
|
27
27
|
import { isTimelineClipActive } from "./useTimelineActiveClips";
|
|
28
28
|
import { queryTimelineClipIndex } from "../lib/timelineClipIndex";
|
|
29
29
|
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
|
30
|
+
import type { TimelineLogicalRow } from "./timelineKeyboardNavigation";
|
|
31
|
+
import { timelineClipFocusId } from "./timelineNavigationIdentity";
|
|
32
|
+
import { useTimelineKeyboardActor } from "./useTimelineKeyboardActor";
|
|
30
33
|
|
|
31
34
|
interface TimelineLanesProps extends TimelineLaneBaseProps {
|
|
32
35
|
/** Live-derived by TimelineCanvas from {@link TimelineLaneBaseProps.draggedClip}. */
|
|
@@ -49,6 +52,8 @@ export function TimelineLanes({
|
|
|
49
52
|
displayTrackOrder,
|
|
50
53
|
rowGeometry,
|
|
51
54
|
virtualRows,
|
|
55
|
+
logicalRows,
|
|
56
|
+
focusedTargetId,
|
|
52
57
|
rowsVirtualized,
|
|
53
58
|
clipIndex,
|
|
54
59
|
renderTimeRange,
|
|
@@ -76,7 +81,6 @@ export function TimelineLanes({
|
|
|
76
81
|
setResizingClip,
|
|
77
82
|
setDraggedClip,
|
|
78
83
|
setSelectedElementId,
|
|
79
|
-
syncClipDragAutoScroll,
|
|
80
84
|
shiftClickClipRef,
|
|
81
85
|
getPreviewElement,
|
|
82
86
|
getTrackStyle,
|
|
@@ -100,14 +104,20 @@ export function TimelineLanes({
|
|
|
100
104
|
onRazorSplit,
|
|
101
105
|
onRazorSplitAll,
|
|
102
106
|
}: TimelineLanesProps) {
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// aria-controls resolve to whichever mounted first. React's useId embeds
|
|
106
|
-
// colons, which are legal in an id and in aria-controls but need escaping in
|
|
107
|
-
// a CSS `#id` selector, so they come out here and the prefix stays plain.
|
|
107
|
+
// ponytail: One per-instance namespace prevents aria-controls and aria-owns
|
|
108
|
+
// from resolving into a second timeline that renders the same logical rows.
|
|
108
109
|
const lanesIdPrefix = `timeline-lanes${useId().replaceAll(":", "")}`;
|
|
109
110
|
const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
|
|
110
111
|
const toggleClipExpanded = usePlayerStore((s) => s.toggleClipExpanded);
|
|
112
|
+
const logicalRowsByTrack = useMemo(() => {
|
|
113
|
+
const byTrack = new Map<number, TimelineLogicalRow[]>();
|
|
114
|
+
for (const logicalRow of logicalRows) {
|
|
115
|
+
const trackRows = byTrack.get(logicalRow.physicalTrackKey) ?? [];
|
|
116
|
+
trackRows.push(logicalRow);
|
|
117
|
+
byTrack.set(logicalRow.physicalTrackKey, trackRows);
|
|
118
|
+
}
|
|
119
|
+
return byTrack;
|
|
120
|
+
}, [logicalRows]);
|
|
111
121
|
const toggleClipExpandedTracked = (key: string) => {
|
|
112
122
|
const willExpand = !expandedClipIds.has(key);
|
|
113
123
|
trackStudioKeyframeLaneExpand({ expanded: willExpand });
|
|
@@ -129,10 +139,23 @@ export function TimelineLanes({
|
|
|
129
139
|
},
|
|
130
140
|
]
|
|
131
141
|
: [];
|
|
142
|
+
const keyboard = useTimelineKeyboardActor({
|
|
143
|
+
logicalRows,
|
|
144
|
+
focusedTargetId,
|
|
145
|
+
rowGeometry,
|
|
146
|
+
scrollRef,
|
|
147
|
+
onToggleRow: (row) => {
|
|
148
|
+
if (row.elementId) toggleClipExpandedTracked(row.elementId);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
132
151
|
return (
|
|
133
152
|
<div
|
|
134
|
-
role="
|
|
153
|
+
role="treegrid"
|
|
135
154
|
aria-label="Timeline tracks"
|
|
155
|
+
aria-rowcount={logicalRows.length}
|
|
156
|
+
aria-colcount={2}
|
|
157
|
+
onFocus={keyboard.onFocus}
|
|
158
|
+
onKeyDown={keyboard.onKeyDown}
|
|
136
159
|
className={rowsVirtualized ? "absolute inset-0" : undefined}
|
|
137
160
|
>
|
|
138
161
|
{
|
|
@@ -141,6 +164,9 @@ export function TimelineLanes({
|
|
|
141
164
|
const trackNum = displayTrackOrder[row];
|
|
142
165
|
if (trackNum === undefined) return null;
|
|
143
166
|
const displayNumber = trackDisplayNumber(displayTrackOrder, trackNum);
|
|
167
|
+
const trackLogicalRows = logicalRowsByTrack.get(trackNum) ?? [];
|
|
168
|
+
const logicalRow = trackLogicalRows[0];
|
|
169
|
+
if (!logicalRow) return null;
|
|
144
170
|
const rowHeight = rowGeometry.getRowHeight(row);
|
|
145
171
|
const els = tracks.find(([t]) => t === trackNum)?.[1] ?? [];
|
|
146
172
|
const renderElements = rowsVirtualized
|
|
@@ -157,9 +183,7 @@ export function TimelineLanes({
|
|
|
157
183
|
draggedClip?.started === true && !trackOrder.includes(trackNum) && els.length === 0;
|
|
158
184
|
// All lanes use the same uniform color — no alternating stripes.
|
|
159
185
|
const rowBackground = theme.rowBackground;
|
|
160
|
-
//
|
|
161
|
-
// or the music track when nothing is selected). When shown, keyframe
|
|
162
|
-
// diamonds shrink + drop to the bottom half so they don't collide with it.
|
|
186
|
+
// Keep diamonds below the beat strip on the active/music track.
|
|
163
187
|
const beatStripOnTrack =
|
|
164
188
|
(beatAnalysis?.beatTimes?.length ?? 0) >= 2 &&
|
|
165
189
|
(selectedElementId
|
|
@@ -167,9 +191,7 @@ export function TimelineLanes({
|
|
|
167
191
|
: els.some(isMusicTrack));
|
|
168
192
|
const isTrackHidden = els.length > 0 && els.every((element) => element.hidden === true);
|
|
169
193
|
const isAudioTrack = els.length > 0 && els.some(isAudioTimelineElement);
|
|
170
|
-
//
|
|
171
|
-
// most lanes). A track can hold several elements; scoping to one keeps
|
|
172
|
-
// their keyframes from cramming into a single row.
|
|
194
|
+
// Only the selected/most-keyframed clip owns expanded lanes on a shared track.
|
|
173
195
|
const keyframeClip = resolveTrackKeyframeClip(
|
|
174
196
|
els,
|
|
175
197
|
laneCounts,
|
|
@@ -179,22 +201,22 @@ export function TimelineLanes({
|
|
|
179
201
|
const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
|
|
180
202
|
const keyframeClipExpanded =
|
|
181
203
|
keyframeClipKey != null && expandedClipIds.has(keyframeClipKey);
|
|
182
|
-
//
|
|
183
|
-
// the disclosure: the caret in the sticky header and the diamond lanes
|
|
184
|
-
// on the canvas. Keyed by display row, not by `trackNum`, which is a
|
|
185
|
-
// fractional sort key and would mint ids like `...-0.16666666666666666`.
|
|
204
|
+
// Link the sticky caret to the canvas lanes with a stable display-row id.
|
|
186
205
|
const lanesId = `${lanesIdPrefix}-track-${row}`;
|
|
187
206
|
return (
|
|
188
207
|
<TimelineTrackRow
|
|
189
208
|
key={rowKey}
|
|
190
209
|
index={row}
|
|
191
210
|
rowKey={rowKey}
|
|
192
|
-
|
|
211
|
+
logicalRow={logicalRow}
|
|
212
|
+
propertyRows={trackLogicalRows.slice(1)}
|
|
213
|
+
lanesId={lanesId}
|
|
193
214
|
top={rowGeometry.getRowTop(row)}
|
|
194
215
|
height={rowHeight}
|
|
195
216
|
virtualized={rowsVirtualized}
|
|
196
217
|
background={rowBackground}
|
|
197
218
|
borderColor={theme.rowBorder}
|
|
219
|
+
rovingTargetId={keyboard.rovingTargetId}
|
|
198
220
|
>
|
|
199
221
|
<TimelineTrackHeader
|
|
200
222
|
trackNumber={trackNum}
|
|
@@ -225,8 +247,11 @@ export function TimelineLanes({
|
|
|
225
247
|
onToggleTrackHidden={onToggleTrackHidden}
|
|
226
248
|
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
|
|
227
249
|
onSeek={onSeek}
|
|
250
|
+
rovingTargetId={keyboard.rovingTargetId}
|
|
228
251
|
/>
|
|
229
252
|
<div
|
|
253
|
+
role="gridcell"
|
|
254
|
+
aria-colindex={2}
|
|
230
255
|
style={{
|
|
231
256
|
width: trackContentWidth,
|
|
232
257
|
marginLeft: contentGutter, // room for a 0% diamond left of t=0
|
|
@@ -298,9 +323,8 @@ export function TimelineLanes({
|
|
|
298
323
|
const isSelected =
|
|
299
324
|
selectedElementId === elementKey || selectedElementIds.has(elementKey);
|
|
300
325
|
const isComposition = !!el.compositionSrc;
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// at/after the change (DOM flash, drag interruption).
|
|
326
|
+
// The element identity is already unique per clip. Never fold in the map
|
|
327
|
+
// index, or a splice/reorder remounts every clip at/after the change.
|
|
304
328
|
const clipKey = elementKey;
|
|
305
329
|
const isDraggingClip =
|
|
306
330
|
draggedClip?.started === true &&
|
|
@@ -308,11 +332,8 @@ export function TimelineLanes({
|
|
|
308
332
|
getTimelineElementIdentity(draggedElement) === elementKey;
|
|
309
333
|
if (isDraggingClip) return null;
|
|
310
334
|
const previewElement = getPreviewElement(el);
|
|
311
|
-
// Passenger of a live multi-drag:
|
|
312
|
-
//
|
|
313
|
-
// compositor transform on a same-geometry wrapper (absolute
|
|
314
|
-
// inset-0 → identical offset parent, so the clip's own
|
|
315
|
-
// left/top are preserved), plus the ghost's elevated z/opacity.
|
|
335
|
+
// Passenger of a live multi-drag: preserve the formation without changing
|
|
336
|
+
// the passenger's timeline data until the owning drag commits.
|
|
316
337
|
const isPassenger =
|
|
317
338
|
multiDragPreview != null && isMultiDragPassenger(clipKey, multiDragPreview);
|
|
318
339
|
const passengerOffsetPx = isPassenger
|
|
@@ -337,6 +358,9 @@ export function TimelineLanes({
|
|
|
337
358
|
capabilities={capabilities}
|
|
338
359
|
theme={theme}
|
|
339
360
|
isComposition={isComposition}
|
|
361
|
+
tabIndex={
|
|
362
|
+
keyboard.rovingTargetId === timelineClipFocusId(elementKey) ? 0 : -1
|
|
363
|
+
}
|
|
340
364
|
onHoverStart={() => setHoveredClip(clipKey)}
|
|
341
365
|
onHoverEnd={() => setHoveredClip(null)}
|
|
342
366
|
onResizeStart={
|
|
@@ -350,6 +374,7 @@ export function TimelineLanes({
|
|
|
350
374
|
setShowPopover(false);
|
|
351
375
|
setRangeSelection(null);
|
|
352
376
|
setResizingClip({
|
|
377
|
+
pointerId: e.pointerId,
|
|
353
378
|
element: el,
|
|
354
379
|
edge,
|
|
355
380
|
originClientX: e.clientX,
|
|
@@ -388,6 +413,7 @@ export function TimelineLanes({
|
|
|
388
413
|
(blockedIntent !== "move" && onResizeElement))
|
|
389
414
|
) {
|
|
390
415
|
blockedClipRef.current = {
|
|
416
|
+
pointerId: e.pointerId,
|
|
391
417
|
element: el,
|
|
392
418
|
intent: blockedIntent,
|
|
393
419
|
originClientX: e.clientX,
|
|
@@ -401,6 +427,7 @@ export function TimelineLanes({
|
|
|
401
427
|
setShowPopover(false);
|
|
402
428
|
setRangeSelection(null);
|
|
403
429
|
setDraggedClip({
|
|
430
|
+
pointerId: e.pointerId,
|
|
404
431
|
element: el,
|
|
405
432
|
originClientX: e.clientX,
|
|
406
433
|
originClientY: e.clientY,
|
|
@@ -418,7 +445,6 @@ export function TimelineLanes({
|
|
|
418
445
|
snapType: null,
|
|
419
446
|
started: false,
|
|
420
447
|
});
|
|
421
|
-
syncClipDragAutoScroll(e.clientX, e.clientY);
|
|
422
448
|
}
|
|
423
449
|
}
|
|
424
450
|
onClick={
|
|
@@ -472,41 +498,33 @@ export function TimelineLanes({
|
|
|
472
498
|
renderClipContent,
|
|
473
499
|
renderClipOverlay,
|
|
474
500
|
)}
|
|
475
|
-
{!showsLanes && keyframeCache?.get(elementKey) && (
|
|
476
|
-
<TimelineClipDiamonds
|
|
477
|
-
keyframesData={keyframeCache.get(elementKey)!}
|
|
478
|
-
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
|
|
479
|
-
clipHeightPx={rowHeight - 2 * CLIP_Y}
|
|
480
|
-
clipDuration={previewElement.duration}
|
|
481
|
-
beatsActive={beatStripOnTrack}
|
|
482
|
-
accentColor={clipStyle.accent}
|
|
483
|
-
isSelected={isSelected}
|
|
484
|
-
currentPercentage={
|
|
485
|
-
previewElement.duration > 0
|
|
486
|
-
? ((currentTime - previewElement.start) / previewElement.duration) *
|
|
487
|
-
100
|
|
488
|
-
: 0
|
|
489
|
-
}
|
|
490
|
-
elementId={elementKey}
|
|
491
|
-
selectedKeyframes={selectedKeyframes}
|
|
492
|
-
onClickKeyframe={(_elId, target) =>
|
|
493
|
-
onClickKeyframe?.(previewElement, target)
|
|
494
|
-
}
|
|
495
|
-
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
496
|
-
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
497
|
-
onMoveKeyframe={onMoveKeyframe}
|
|
498
|
-
onSelectSegment={onSelectSegment}
|
|
499
|
-
suppressClickRef={suppressClickRef}
|
|
500
|
-
/>
|
|
501
|
-
)}
|
|
502
501
|
</TimelineClip>
|
|
503
502
|
);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
503
|
+
const compactKeyframes = keyframeCache?.get(elementKey);
|
|
504
|
+
const compactDiamonds = !showsLanes && compactKeyframes && (
|
|
505
|
+
<TimelineCompactDiamonds
|
|
506
|
+
key={`${clipKey}-diamonds`}
|
|
507
|
+
element={previewElement}
|
|
508
|
+
elementId={elementKey}
|
|
509
|
+
keyframesData={compactKeyframes}
|
|
510
|
+
pixelsPerSecond={pps}
|
|
511
|
+
rowHeight={rowHeight}
|
|
512
|
+
beatsActive={beatStripOnTrack}
|
|
513
|
+
accentColor={clipStyle.accent}
|
|
514
|
+
isSelected={isSelected}
|
|
515
|
+
currentTime={currentTime}
|
|
516
|
+
selectedKeyframes={selectedKeyframes}
|
|
517
|
+
rovingTargetId={keyboard.rovingTargetId}
|
|
518
|
+
onClickKeyframe={onClickKeyframe}
|
|
519
|
+
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
520
|
+
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
521
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
522
|
+
onSelectSegment={onSelectSegment}
|
|
523
|
+
suppressClickRef={suppressClickRef}
|
|
524
|
+
/>
|
|
525
|
+
);
|
|
526
|
+
// Keep this shell mounted while collapsed so aria-controls stays valid
|
|
527
|
+
// and multi-drag cannot remount the subtree mid-gesture.
|
|
510
528
|
const propertyLanes = isTrackKeyframeClip && (
|
|
511
529
|
<TimelinePropertyLanes
|
|
512
530
|
key={`${clipKey}-property-lanes`}
|
|
@@ -528,6 +546,7 @@ export function TimelineLanes({
|
|
|
528
546
|
}
|
|
529
547
|
elementId={elementKey}
|
|
530
548
|
selectedKeyframes={selectedKeyframes}
|
|
549
|
+
rovingTargetId={keyboard.rovingTargetId}
|
|
531
550
|
onSelectSegment={(target) => onSelectSegment?.(elementKey, target)}
|
|
532
551
|
onClickKeyframe={(target) => onClickKeyframe?.(previewElement, target)}
|
|
533
552
|
onShiftClickKeyframe={(target) =>
|
|
@@ -543,14 +562,12 @@ export function TimelineLanes({
|
|
|
543
562
|
suppressClickRef={suppressClickRef}
|
|
544
563
|
/>
|
|
545
564
|
);
|
|
546
|
-
//
|
|
547
|
-
// array here makes React reconcile the outer array by
|
|
548
|
-
// position, so a window shift remounts otherwise stable
|
|
549
|
-
// clip keys and can tear down focus mid-reveal.
|
|
565
|
+
// One keyed child prevents window shifts from remounting focused clips.
|
|
550
566
|
if (!isPassenger) {
|
|
551
567
|
return (
|
|
552
568
|
<Fragment key={clipKey}>
|
|
553
569
|
{clip}
|
|
570
|
+
{compactDiamonds}
|
|
554
571
|
{propertyLanes}
|
|
555
572
|
</Fragment>
|
|
556
573
|
);
|
|
@@ -567,6 +584,7 @@ export function TimelineLanes({
|
|
|
567
584
|
}}
|
|
568
585
|
>
|
|
569
586
|
{clip}
|
|
587
|
+
{compactDiamonds}
|
|
570
588
|
{propertyLanes}
|
|
571
589
|
</div>
|
|
572
590
|
);
|