@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,303 @@
|
|
|
1
|
+
import type { GsapAnimation, PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
4
|
+
import {
|
|
5
|
+
buildTimelineLogicalRows,
|
|
6
|
+
resolveTimelineFocusFallback,
|
|
7
|
+
resolveTimelineNavigationTarget,
|
|
8
|
+
} from "./timelineKeyboardNavigation";
|
|
9
|
+
import { timelineClipFocusId, timelineTrackRowId } from "./timelineNavigationIdentity";
|
|
10
|
+
|
|
11
|
+
function clip(id: string, track: number, start: number, duration = 2): TimelineElement {
|
|
12
|
+
return { id, track, start, duration, tag: "div" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fallbackTracks(firstTrack: TimelineElement[]): [number, TimelineElement[]][] {
|
|
16
|
+
return [
|
|
17
|
+
[1, firstTrack],
|
|
18
|
+
[2, []],
|
|
19
|
+
[3, [clip("right", 3, 18), clip("left", 3, 2)]],
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function animation(
|
|
24
|
+
id: string,
|
|
25
|
+
group: PropertyGroupName,
|
|
26
|
+
percentages: readonly number[],
|
|
27
|
+
resolvedStart = 10,
|
|
28
|
+
): GsapAnimation {
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
targetSelector: "#active",
|
|
32
|
+
method: "to",
|
|
33
|
+
position: 0,
|
|
34
|
+
resolvedStart,
|
|
35
|
+
duration: 10,
|
|
36
|
+
properties: {},
|
|
37
|
+
propertyGroup: group,
|
|
38
|
+
keyframes: {
|
|
39
|
+
format: "percentage",
|
|
40
|
+
keyframes: percentages.map((percentage) => ({
|
|
41
|
+
percentage,
|
|
42
|
+
properties: group === "position" ? { x: percentage } : { opacity: percentage / 100 },
|
|
43
|
+
})),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function model(overrides: Partial<Parameters<typeof buildTimelineLogicalRows>[0]> = {}) {
|
|
49
|
+
const active = clip("active", 1, 10, 10);
|
|
50
|
+
return buildTimelineLogicalRows({
|
|
51
|
+
tracks: [
|
|
52
|
+
[1, [clip("late", 1, 20), active, clip("early", 1, 0)]],
|
|
53
|
+
[2, []],
|
|
54
|
+
[3, [clip("right", 3, 18), clip("left", 3, 2)]],
|
|
55
|
+
],
|
|
56
|
+
displayTrackOrder: [1, 2, 3],
|
|
57
|
+
laneCounts: new Map([["active", 2]]),
|
|
58
|
+
selectedElementId: "active",
|
|
59
|
+
selectedElementIds: new Set(),
|
|
60
|
+
expandedClipIds: new Set(["active"]),
|
|
61
|
+
gsapAnimations: new Map([
|
|
62
|
+
[
|
|
63
|
+
"active",
|
|
64
|
+
[animation("position", "position", [0, 50, 100]), animation("visual", "visual", [25, 75])],
|
|
65
|
+
],
|
|
66
|
+
]),
|
|
67
|
+
...overrides,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("buildTimelineLogicalRows", () => {
|
|
72
|
+
it("projects tracks, empty tracks, and expanded property rows with continuous indices", () => {
|
|
73
|
+
const rows = model();
|
|
74
|
+
|
|
75
|
+
expect(
|
|
76
|
+
rows.map(({ physicalTrackKey, logicalIndex, level, parentId, expandable }) => ({
|
|
77
|
+
physicalTrackKey,
|
|
78
|
+
logicalIndex,
|
|
79
|
+
level,
|
|
80
|
+
parentId,
|
|
81
|
+
expandable,
|
|
82
|
+
})),
|
|
83
|
+
).toEqual([
|
|
84
|
+
{ physicalTrackKey: 1, logicalIndex: 0, level: 1, parentId: null, expandable: true },
|
|
85
|
+
{
|
|
86
|
+
physicalTrackKey: 1,
|
|
87
|
+
logicalIndex: 1,
|
|
88
|
+
level: 2,
|
|
89
|
+
parentId: timelineTrackRowId(1),
|
|
90
|
+
expandable: false,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
physicalTrackKey: 1,
|
|
94
|
+
logicalIndex: 2,
|
|
95
|
+
level: 2,
|
|
96
|
+
parentId: timelineTrackRowId(1),
|
|
97
|
+
expandable: false,
|
|
98
|
+
},
|
|
99
|
+
{ physicalTrackKey: 2, logicalIndex: 3, level: 1, parentId: null, expandable: false },
|
|
100
|
+
{ physicalTrackKey: 3, logicalIndex: 4, level: 1, parentId: null, expandable: false },
|
|
101
|
+
]);
|
|
102
|
+
expect(rows[0]?.expanded).toBe(true);
|
|
103
|
+
expect(rows[3]?.items).toEqual([]);
|
|
104
|
+
expect(rows[0]?.items.map((item) => item.elementId)).toEqual(["early", "active", "late"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("orders keyframes and their segment ease controls deterministically", () => {
|
|
108
|
+
const rows = model({
|
|
109
|
+
gsapAnimations: new Map([
|
|
110
|
+
[
|
|
111
|
+
"active",
|
|
112
|
+
[
|
|
113
|
+
animation("z-animation", "position", [100, 0, 50]),
|
|
114
|
+
animation("a-animation", "position", [50]),
|
|
115
|
+
],
|
|
116
|
+
],
|
|
117
|
+
]),
|
|
118
|
+
});
|
|
119
|
+
const position = rows.find((row) => row.propertyGroup === "position")!;
|
|
120
|
+
|
|
121
|
+
expect(
|
|
122
|
+
position.items.map((item) => [item.kind, item.time, item.keyframeTarget?.animationId]),
|
|
123
|
+
).toEqual([
|
|
124
|
+
["keyframe", 10, "z-animation"],
|
|
125
|
+
["ease", 12.5, "a-animation"],
|
|
126
|
+
["keyframe", 15, "a-animation"],
|
|
127
|
+
["keyframe", 15, "z-animation"],
|
|
128
|
+
["ease", 17.5, "z-animation"],
|
|
129
|
+
["keyframe", 20, "z-animation"],
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("uses the selected keyframed clip as the sole expanded-lane owner", () => {
|
|
134
|
+
const other = clip("other", 1, 0, 4);
|
|
135
|
+
const rows = model({
|
|
136
|
+
tracks: [[1, [other, clip("active", 1, 10, 10)]]],
|
|
137
|
+
displayTrackOrder: [1],
|
|
138
|
+
laneCounts: new Map([
|
|
139
|
+
["active", 1],
|
|
140
|
+
["other", 1],
|
|
141
|
+
]),
|
|
142
|
+
selectedElementId: "other",
|
|
143
|
+
expandedClipIds: new Set(["other", "active"]),
|
|
144
|
+
gsapAnimations: new Map([
|
|
145
|
+
["active", [animation("active-position", "position", [0, 100])]],
|
|
146
|
+
["other", [animation("other-visual", "visual", [0, 100], 0)]],
|
|
147
|
+
]),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(rows.map((row) => row.propertyGroup).filter(Boolean)).toEqual(["visual"]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("resolveTimelineNavigationTarget", () => {
|
|
155
|
+
it("navigates horizontal items plus row Home and End", () => {
|
|
156
|
+
const rows = model();
|
|
157
|
+
const activeId = timelineClipFocusId("active");
|
|
158
|
+
|
|
159
|
+
expect(resolveTimelineNavigationTarget(rows, activeId, "ArrowLeft")?.id).toBe(
|
|
160
|
+
timelineClipFocusId("early"),
|
|
161
|
+
);
|
|
162
|
+
expect(resolveTimelineNavigationTarget(rows, activeId, "ArrowRight")?.id).toBe(
|
|
163
|
+
timelineClipFocusId("late"),
|
|
164
|
+
);
|
|
165
|
+
expect(resolveTimelineNavigationTarget(rows, timelineTrackRowId(1), "ArrowLeft")?.id).toBe(
|
|
166
|
+
timelineTrackRowId(1),
|
|
167
|
+
);
|
|
168
|
+
expect(
|
|
169
|
+
resolveTimelineNavigationTarget(rows, timelineClipFocusId("late"), "ArrowRight")?.id,
|
|
170
|
+
).toBe(timelineClipFocusId("late"));
|
|
171
|
+
expect(resolveTimelineNavigationTarget(rows, timelineTrackRowId(1), "ArrowRight")?.id).toBe(
|
|
172
|
+
timelineClipFocusId("early"),
|
|
173
|
+
);
|
|
174
|
+
expect(
|
|
175
|
+
resolveTimelineNavigationTarget(rows, timelineClipFocusId("early"), "ArrowLeft")?.id,
|
|
176
|
+
).toBe(timelineTrackRowId(1));
|
|
177
|
+
expect(resolveTimelineNavigationTarget(rows, activeId, "Home")?.id).toBe(timelineTrackRowId(1));
|
|
178
|
+
expect(resolveTimelineNavigationTarget(rows, timelineTrackRowId(1), "End")?.id).toBe(
|
|
179
|
+
timelineClipFocusId("late"),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("navigates every logical row including properties and empty tracks", () => {
|
|
184
|
+
const rows = model();
|
|
185
|
+
const activeId = timelineClipFocusId("active");
|
|
186
|
+
const propertyTarget = resolveTimelineNavigationTarget(rows, activeId, "ArrowDown")!;
|
|
187
|
+
|
|
188
|
+
expect(propertyTarget.kind).toBe("keyframe");
|
|
189
|
+
expect(propertyTarget.time).toBe(15);
|
|
190
|
+
expect(
|
|
191
|
+
resolveTimelineNavigationTarget(rows, propertyTarget.id, "PageDown", { pageSize: 2 })?.id,
|
|
192
|
+
).toBe(timelineTrackRowId(2));
|
|
193
|
+
expect(resolveTimelineNavigationTarget(rows, timelineTrackRowId(2), "ArrowDown")?.id).toBe(
|
|
194
|
+
timelineTrackRowId(3),
|
|
195
|
+
);
|
|
196
|
+
expect(resolveTimelineNavigationTarget(rows, propertyTarget.id, "ArrowUp")?.id).toBe(activeId);
|
|
197
|
+
expect(
|
|
198
|
+
resolveTimelineNavigationTarget(rows, propertyTarget.id, "PageUp", { pageSize: 2 })?.id,
|
|
199
|
+
).toBe(activeId);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("uses a caller-supplied page size and ignores invalid page commands", () => {
|
|
203
|
+
const rows = model();
|
|
204
|
+
const current = timelineTrackRowId(1);
|
|
205
|
+
|
|
206
|
+
expect(resolveTimelineNavigationTarget(rows, current, "PageDown")?.id).toBe(current);
|
|
207
|
+
expect(resolveTimelineNavigationTarget(rows, current, "PageDown", { pageSize: 3 })?.id).toBe(
|
|
208
|
+
timelineTrackRowId(2),
|
|
209
|
+
);
|
|
210
|
+
expect(
|
|
211
|
+
resolveTimelineNavigationTarget(rows, timelineTrackRowId(3), "PageDown", { pageSize: 1 })?.id,
|
|
212
|
+
).toBe(timelineTrackRowId(3));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("supports modified Home and End across the whole logical model", () => {
|
|
216
|
+
const rows = model();
|
|
217
|
+
const current = timelineTrackRowId(2);
|
|
218
|
+
|
|
219
|
+
expect(
|
|
220
|
+
resolveTimelineNavigationTarget(rows, current, "Home", { timelineBoundary: true })?.id,
|
|
221
|
+
).toBe(timelineTrackRowId(1));
|
|
222
|
+
expect(
|
|
223
|
+
resolveTimelineNavigationTarget(rows, current, "End", { timelineBoundary: true })?.id,
|
|
224
|
+
).toBe(timelineTrackRowId(3));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("returns from a property row to its parent with ArrowLeft", () => {
|
|
228
|
+
const rows = model();
|
|
229
|
+
const property = rows.find((row) => row.propertyGroup === "position")!;
|
|
230
|
+
|
|
231
|
+
expect(resolveTimelineNavigationTarget(rows, property.id, "ArrowLeft")?.id).toBe(
|
|
232
|
+
timelineTrackRowId(1),
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("breaks equal-distance vertical ties by time then stable identity", () => {
|
|
237
|
+
const rows = buildTimelineLogicalRows({
|
|
238
|
+
tracks: [
|
|
239
|
+
[1, [clip("current", 1, 9, 2)]],
|
|
240
|
+
[2, [clip("later", 2, 14, 2), clip("earlier-z", 2, 4, 2), clip("earlier-a", 2, 4, 2)]],
|
|
241
|
+
],
|
|
242
|
+
displayTrackOrder: [1, 2],
|
|
243
|
+
laneCounts: new Map(),
|
|
244
|
+
selectedElementId: null,
|
|
245
|
+
selectedElementIds: new Set(),
|
|
246
|
+
expandedClipIds: new Set(),
|
|
247
|
+
gsapAnimations: new Map(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(
|
|
251
|
+
resolveTimelineNavigationTarget(rows, timelineClipFocusId("current"), "ArrowDown")?.id,
|
|
252
|
+
).toBe(timelineClipFocusId("earlier-a"));
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("resolveTimelineFocusFallback", () => {
|
|
257
|
+
it("chooses previous, then next, then the containing row after deletion", () => {
|
|
258
|
+
const before = model({ expandedClipIds: new Set() });
|
|
259
|
+
const withoutActive = model({
|
|
260
|
+
expandedClipIds: new Set(),
|
|
261
|
+
tracks: fallbackTracks([clip("early", 1, 0), clip("late", 1, 20)]),
|
|
262
|
+
});
|
|
263
|
+
expect(
|
|
264
|
+
resolveTimelineFocusFallback(before, withoutActive, timelineClipFocusId("active"))?.id,
|
|
265
|
+
).toBe(timelineClipFocusId("early"));
|
|
266
|
+
|
|
267
|
+
const onlyNext = model({
|
|
268
|
+
expandedClipIds: new Set(),
|
|
269
|
+
tracks: fallbackTracks([clip("late", 1, 20)]),
|
|
270
|
+
});
|
|
271
|
+
expect(resolveTimelineFocusFallback(before, onlyNext, timelineClipFocusId("active"))?.id).toBe(
|
|
272
|
+
timelineClipFocusId("late"),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const onlyActive = model({
|
|
276
|
+
expandedClipIds: new Set(),
|
|
277
|
+
tracks: [[1, [clip("active", 1, 10, 10)]]],
|
|
278
|
+
displayTrackOrder: [1],
|
|
279
|
+
});
|
|
280
|
+
const empty = model({
|
|
281
|
+
expandedClipIds: new Set(),
|
|
282
|
+
tracks: [[1, []]],
|
|
283
|
+
displayTrackOrder: [1],
|
|
284
|
+
});
|
|
285
|
+
expect(resolveTimelineFocusFallback(onlyActive, empty, timelineClipFocusId("active"))?.id).toBe(
|
|
286
|
+
timelineTrackRowId(1),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("falls back from a collapsed property row to its parent track", () => {
|
|
291
|
+
const before = model();
|
|
292
|
+
const property = before.find((row) => row.propertyGroup === "position")!;
|
|
293
|
+
const after = model({ expandedClipIds: new Set() });
|
|
294
|
+
|
|
295
|
+
expect(resolveTimelineFocusFallback(before, after, property.items[0]!.id)?.id).toBe(
|
|
296
|
+
timelineTrackRowId(1),
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("returns null for an identity absent from the previous model", () => {
|
|
301
|
+
expect(resolveTimelineFocusFallback(model(), model(), "missing")).toBeNull();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import type { GsapAnimation, PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
+
import { getTimelinePropertyLanes } from "./TimelinePropertyLanes";
|
|
4
|
+
import {
|
|
5
|
+
timelineKeyframeSelectionKey,
|
|
6
|
+
type TimelineKeyframeTarget,
|
|
7
|
+
} from "./timelineKeyframeIdentity";
|
|
8
|
+
import {
|
|
9
|
+
timelineClipFocusId,
|
|
10
|
+
timelineEaseFocusId,
|
|
11
|
+
timelineKeyframeFocusId,
|
|
12
|
+
timelinePropertyRowId,
|
|
13
|
+
timelineTrackRowId,
|
|
14
|
+
} from "./timelineNavigationIdentity";
|
|
15
|
+
import { resolveTrackKeyframeClip } from "./useTimelineTrackLayout";
|
|
16
|
+
|
|
17
|
+
export type TimelineNavigationKey =
|
|
18
|
+
| "ArrowLeft"
|
|
19
|
+
| "ArrowRight"
|
|
20
|
+
| "ArrowUp"
|
|
21
|
+
| "ArrowDown"
|
|
22
|
+
| "Home"
|
|
23
|
+
| "End"
|
|
24
|
+
| "PageUp"
|
|
25
|
+
| "PageDown";
|
|
26
|
+
|
|
27
|
+
const NAVIGATION_KEYS: ReadonlySet<string> = new Set<TimelineNavigationKey>([
|
|
28
|
+
"ArrowLeft",
|
|
29
|
+
"ArrowRight",
|
|
30
|
+
"ArrowUp",
|
|
31
|
+
"ArrowDown",
|
|
32
|
+
"Home",
|
|
33
|
+
"End",
|
|
34
|
+
"PageUp",
|
|
35
|
+
"PageDown",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export function isTimelineNavigationKey(key: string): key is TimelineNavigationKey {
|
|
39
|
+
return NAVIGATION_KEYS.has(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TimelineLogicalItem {
|
|
43
|
+
id: string;
|
|
44
|
+
kind: "clip" | "keyframe" | "ease";
|
|
45
|
+
rowId: string;
|
|
46
|
+
elementId: string;
|
|
47
|
+
/** The item's time anchor. Clips use their midpoint; ease controls use the segment midpoint. */
|
|
48
|
+
time: number;
|
|
49
|
+
keyframeTarget?: TimelineKeyframeTarget;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TimelineLogicalRow {
|
|
53
|
+
id: string;
|
|
54
|
+
kind: "row";
|
|
55
|
+
physicalTrackKey: number;
|
|
56
|
+
logicalIndex: number;
|
|
57
|
+
level: 1 | 2;
|
|
58
|
+
parentId: string | null;
|
|
59
|
+
elementId: string | null;
|
|
60
|
+
expandable: boolean;
|
|
61
|
+
expanded: boolean;
|
|
62
|
+
propertyGroup?: PropertyGroupName;
|
|
63
|
+
items: readonly TimelineLogicalItem[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type TimelineLogicalTarget = TimelineLogicalRow | TimelineLogicalItem;
|
|
67
|
+
|
|
68
|
+
interface BuildTimelineLogicalRowsInput {
|
|
69
|
+
tracks: readonly (readonly [number, readonly TimelineElement[]])[];
|
|
70
|
+
displayTrackOrder: readonly number[];
|
|
71
|
+
laneCounts: ReadonlyMap<string, number>;
|
|
72
|
+
selectedElementId: string | null;
|
|
73
|
+
selectedElementIds: ReadonlySet<string>;
|
|
74
|
+
expandedClipIds: ReadonlySet<string>;
|
|
75
|
+
gsapAnimations: ReadonlyMap<string, readonly GsapAnimation[]>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TimelineNavigationOptions {
|
|
79
|
+
/** Supplied by the viewport actor; the model never guesses a fixed page size. */
|
|
80
|
+
pageSize?: number;
|
|
81
|
+
/** Ctrl/Meta + Home/End moves to the first/last logical row. */
|
|
82
|
+
timelineBoundary?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function elementId(element: TimelineElement): string {
|
|
86
|
+
return element.key ?? element.id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clipItems(rowId: string, elements: readonly TimelineElement[]): TimelineLogicalItem[] {
|
|
90
|
+
return [...elements]
|
|
91
|
+
.sort(
|
|
92
|
+
(left, right) =>
|
|
93
|
+
left.start - right.start ||
|
|
94
|
+
left.start + left.duration - (right.start + right.duration) ||
|
|
95
|
+
elementId(left).localeCompare(elementId(right)),
|
|
96
|
+
)
|
|
97
|
+
.map((element) => {
|
|
98
|
+
const id = elementId(element);
|
|
99
|
+
return {
|
|
100
|
+
id: timelineClipFocusId(id),
|
|
101
|
+
kind: "clip",
|
|
102
|
+
rowId,
|
|
103
|
+
elementId: id,
|
|
104
|
+
time: element.start + element.duration / 2,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function keyframeTarget(
|
|
110
|
+
keyframe: ReturnType<typeof getTimelinePropertyLanes>[number]["keyframes"][number],
|
|
111
|
+
): TimelineKeyframeTarget {
|
|
112
|
+
return {
|
|
113
|
+
percentage: keyframe.percentage,
|
|
114
|
+
tweenPercentage: keyframe.tweenPercentage,
|
|
115
|
+
propertyGroup: keyframe.propertyGroup,
|
|
116
|
+
animationId: keyframe.animationId,
|
|
117
|
+
collidingAnimationTargets: keyframe.collidingAnimationTargets,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function propertyItems(
|
|
122
|
+
rowId: string,
|
|
123
|
+
clip: TimelineElement,
|
|
124
|
+
keyframes: ReturnType<typeof getTimelinePropertyLanes>[number]["keyframes"],
|
|
125
|
+
): TimelineLogicalItem[] {
|
|
126
|
+
const id = elementId(clip);
|
|
127
|
+
const unique = new Map<string, { target: TimelineKeyframeTarget; time: number }>();
|
|
128
|
+
for (const keyframe of keyframes) {
|
|
129
|
+
const target = keyframeTarget(keyframe);
|
|
130
|
+
const key = timelineKeyframeSelectionKey(id, target);
|
|
131
|
+
if (!unique.has(key)) {
|
|
132
|
+
unique.set(key, {
|
|
133
|
+
target,
|
|
134
|
+
time: clip.start + (keyframe.percentage / 100) * clip.duration,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const ordered = [...unique.entries()].sort(
|
|
139
|
+
([leftKey, left], [rightKey, right]) =>
|
|
140
|
+
left.time - right.time || leftKey.localeCompare(rightKey),
|
|
141
|
+
);
|
|
142
|
+
const items: TimelineLogicalItem[] = [];
|
|
143
|
+
// ponytail: The composite property lane owns adjacency, so the incoming keyframe
|
|
144
|
+
// owns an ease segment even when its previous neighbor came from another animation.
|
|
145
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
146
|
+
const [, current] = ordered[index]!;
|
|
147
|
+
const previous = ordered[index - 1]?.[1];
|
|
148
|
+
if (previous && current.time > previous.time && current.target.animationId !== undefined) {
|
|
149
|
+
items.push({
|
|
150
|
+
id: timelineEaseFocusId(id, current.target),
|
|
151
|
+
kind: "ease",
|
|
152
|
+
rowId,
|
|
153
|
+
elementId: id,
|
|
154
|
+
time: previous.time + (current.time - previous.time) / 2,
|
|
155
|
+
keyframeTarget: current.target,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
items.push({
|
|
159
|
+
id: timelineKeyframeFocusId(id, current.target),
|
|
160
|
+
kind: "keyframe",
|
|
161
|
+
rowId,
|
|
162
|
+
elementId: id,
|
|
163
|
+
time: current.time,
|
|
164
|
+
keyframeTarget: current.target,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return items;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Canonical model of the treegrid, independent of which virtual rows or clips are mounted. */
|
|
171
|
+
export function buildTimelineLogicalRows({
|
|
172
|
+
tracks,
|
|
173
|
+
displayTrackOrder,
|
|
174
|
+
laneCounts,
|
|
175
|
+
selectedElementId,
|
|
176
|
+
selectedElementIds,
|
|
177
|
+
expandedClipIds,
|
|
178
|
+
gsapAnimations,
|
|
179
|
+
}: BuildTimelineLogicalRowsInput): TimelineLogicalRow[] {
|
|
180
|
+
const trackMap = new Map(tracks);
|
|
181
|
+
const rows: TimelineLogicalRow[] = [];
|
|
182
|
+
for (const track of displayTrackOrder) {
|
|
183
|
+
const elements = trackMap.get(track) ?? [];
|
|
184
|
+
const trackId = timelineTrackRowId(track);
|
|
185
|
+
const activeClip = resolveTrackKeyframeClip(
|
|
186
|
+
elements,
|
|
187
|
+
laneCounts,
|
|
188
|
+
selectedElementId,
|
|
189
|
+
selectedElementIds,
|
|
190
|
+
);
|
|
191
|
+
const activeId = activeClip ? elementId(activeClip) : null;
|
|
192
|
+
const lanes = activeClip
|
|
193
|
+
? getTimelinePropertyLanes(
|
|
194
|
+
gsapAnimations.get(elementId(activeClip)) ?? [],
|
|
195
|
+
activeClip.start,
|
|
196
|
+
activeClip.duration,
|
|
197
|
+
)
|
|
198
|
+
: [];
|
|
199
|
+
const expanded = activeId !== null && expandedClipIds.has(activeId) && lanes.length > 0;
|
|
200
|
+
rows.push({
|
|
201
|
+
id: trackId,
|
|
202
|
+
kind: "row",
|
|
203
|
+
physicalTrackKey: track,
|
|
204
|
+
logicalIndex: rows.length,
|
|
205
|
+
level: 1,
|
|
206
|
+
parentId: null,
|
|
207
|
+
elementId: activeId,
|
|
208
|
+
expandable: lanes.length > 0,
|
|
209
|
+
expanded,
|
|
210
|
+
items: clipItems(trackId, elements),
|
|
211
|
+
});
|
|
212
|
+
if (!expanded || !activeClip) continue;
|
|
213
|
+
for (const lane of lanes) {
|
|
214
|
+
const rowId = timelinePropertyRowId(activeId, lane.group);
|
|
215
|
+
rows.push({
|
|
216
|
+
id: rowId,
|
|
217
|
+
kind: "row",
|
|
218
|
+
physicalTrackKey: track,
|
|
219
|
+
logicalIndex: rows.length,
|
|
220
|
+
level: 2,
|
|
221
|
+
parentId: trackId,
|
|
222
|
+
elementId: activeId,
|
|
223
|
+
expandable: false,
|
|
224
|
+
expanded: false,
|
|
225
|
+
propertyGroup: lane.group,
|
|
226
|
+
items: propertyItems(rowId, activeClip, lane.keyframes),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return rows;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function locateTimelineLogicalTarget(rows: readonly TimelineLogicalRow[], id: string) {
|
|
234
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
235
|
+
const row = rows[rowIndex]!;
|
|
236
|
+
if (row.id === id) return { row, rowIndex, itemIndex: -1, target: row };
|
|
237
|
+
const itemIndex = row.items.findIndex((item) => item.id === id);
|
|
238
|
+
if (itemIndex >= 0) return { row, rowIndex, itemIndex, target: row.items[itemIndex]! };
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function nearestTarget(row: TimelineLogicalRow, time: number): TimelineLogicalTarget {
|
|
244
|
+
return (
|
|
245
|
+
[...row.items].sort(
|
|
246
|
+
(left, right) =>
|
|
247
|
+
Math.abs(left.time - time) - Math.abs(right.time - time) ||
|
|
248
|
+
left.time - right.time ||
|
|
249
|
+
left.id.localeCompare(right.id),
|
|
250
|
+
)[0] ?? row
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve the next logical target without touching the DOM. The downstream
|
|
256
|
+
* `useTimelineKeyboardActor` hook consumes this model when keyboard controls
|
|
257
|
+
* are wired to the rendered timeline.
|
|
258
|
+
*/
|
|
259
|
+
// The branching is the keyboard contract: four key classes intentionally share one actor.
|
|
260
|
+
// fallow-ignore-next-line complexity
|
|
261
|
+
export function resolveTimelineNavigationTarget(
|
|
262
|
+
rows: readonly TimelineLogicalRow[],
|
|
263
|
+
currentId: string,
|
|
264
|
+
key: TimelineNavigationKey,
|
|
265
|
+
options: TimelineNavigationOptions = {},
|
|
266
|
+
): TimelineLogicalTarget | null {
|
|
267
|
+
const current = locateTimelineLogicalTarget(rows, currentId);
|
|
268
|
+
if (!current) return null;
|
|
269
|
+
const { row, rowIndex, itemIndex, target } = current;
|
|
270
|
+
|
|
271
|
+
if (key === "Home" || key === "End") {
|
|
272
|
+
const boundaryRow = options.timelineBoundary ? (key === "Home" ? rows[0] : rows.at(-1)) : row;
|
|
273
|
+
if (!boundaryRow) return target;
|
|
274
|
+
if (options.timelineBoundary) return boundaryRow;
|
|
275
|
+
return key === "Home" ? boundaryRow : (boundaryRow.items.at(-1) ?? boundaryRow);
|
|
276
|
+
}
|
|
277
|
+
if (key === "ArrowLeft") {
|
|
278
|
+
if (itemIndex < 0) {
|
|
279
|
+
if (!row.parentId) return row;
|
|
280
|
+
return locateTimelineLogicalTarget(rows, row.parentId)?.target ?? row;
|
|
281
|
+
}
|
|
282
|
+
return itemIndex === 0 ? row : row.items[itemIndex - 1]!;
|
|
283
|
+
}
|
|
284
|
+
if (key === "ArrowRight") {
|
|
285
|
+
if (itemIndex < 0) return row.items[0] ?? row;
|
|
286
|
+
return row.items[itemIndex + 1] ?? target;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const direction = key === "ArrowUp" || key === "PageUp" ? -1 : 1;
|
|
290
|
+
const pageKey = key === "PageUp" || key === "PageDown";
|
|
291
|
+
const pageSize = options.pageSize;
|
|
292
|
+
if (pageKey && (pageSize === undefined || !Number.isFinite(pageSize) || pageSize < 1)) {
|
|
293
|
+
return target;
|
|
294
|
+
}
|
|
295
|
+
const distance = pageKey ? Math.floor(pageSize!) : 1;
|
|
296
|
+
const destinationIndex = Math.max(0, Math.min(rows.length - 1, rowIndex + direction * distance));
|
|
297
|
+
const destination = rows[destinationIndex];
|
|
298
|
+
if (!destination || destinationIndex === rowIndex) return target;
|
|
299
|
+
return target.kind === "row" ? destination : nearestTarget(destination, target.time);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Preserve focus when possible, then choose previous, next, parent, or the
|
|
304
|
+
* nearest surviving row. The downstream `useTimelineFocusCoordinator` hook
|
|
305
|
+
* consumes this fallback when virtualization unmounts a logical target.
|
|
306
|
+
*/
|
|
307
|
+
// The ordered fallback chain is the invariant; splitting it would duplicate traversal state.
|
|
308
|
+
// fallow-ignore-next-line complexity
|
|
309
|
+
export function resolveTimelineFocusFallback(
|
|
310
|
+
previousRows: readonly TimelineLogicalRow[],
|
|
311
|
+
nextRows: readonly TimelineLogicalRow[],
|
|
312
|
+
currentId: string,
|
|
313
|
+
): TimelineLogicalTarget | null {
|
|
314
|
+
const unchanged = locateTimelineLogicalTarget(nextRows, currentId);
|
|
315
|
+
if (unchanged) return unchanged.target;
|
|
316
|
+
const previous = locateTimelineLogicalTarget(previousRows, currentId);
|
|
317
|
+
if (!previous) return null;
|
|
318
|
+
|
|
319
|
+
if (previous.itemIndex >= 0) {
|
|
320
|
+
for (let index = previous.itemIndex - 1; index >= 0; index -= 1) {
|
|
321
|
+
const candidate = locateTimelineLogicalTarget(nextRows, previous.row.items[index]!.id);
|
|
322
|
+
if (candidate) return candidate.target;
|
|
323
|
+
}
|
|
324
|
+
for (let index = previous.itemIndex + 1; index < previous.row.items.length; index += 1) {
|
|
325
|
+
const candidate = locateTimelineLogicalTarget(nextRows, previous.row.items[index]!.id);
|
|
326
|
+
if (candidate) return candidate.target;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const survivingRow = locateTimelineLogicalTarget(nextRows, previous.row.id);
|
|
331
|
+
if (survivingRow) return survivingRow.target;
|
|
332
|
+
if (previous.row.parentId) {
|
|
333
|
+
const parent = locateTimelineLogicalTarget(nextRows, previous.row.parentId);
|
|
334
|
+
if (parent) return parent.target;
|
|
335
|
+
}
|
|
336
|
+
return nextRows[previous.rowIndex] ?? nextRows[previous.rowIndex - 1] ?? null;
|
|
337
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./us
|
|
|
9
9
|
import type { TimelineClipIndex, TimelineTimeRange } from "../lib/timelineClipIndex";
|
|
10
10
|
import type { TimelineRowGeometry } from "./timelineLayout";
|
|
11
11
|
import type { TimelineVirtualRow } from "./useTimelineVirtualRows";
|
|
12
|
+
import type { TimelineLogicalRow } from "./timelineKeyboardNavigation";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Props shared by the scroll container ({@link import("./TimelineCanvas")}) and
|
|
@@ -27,6 +28,8 @@ export interface TimelineLaneBaseProps {
|
|
|
27
28
|
rowHeights: readonly number[];
|
|
28
29
|
rowGeometry: TimelineRowGeometry;
|
|
29
30
|
virtualRows: readonly TimelineVirtualRow[];
|
|
31
|
+
logicalRows: readonly TimelineLogicalRow[];
|
|
32
|
+
focusedTargetId: string | null;
|
|
30
33
|
rowsVirtualized: boolean;
|
|
31
34
|
clipIndex: TimelineClipIndex;
|
|
32
35
|
renderTimeRange: TimelineTimeRange;
|
|
@@ -55,7 +58,6 @@ export interface TimelineLaneBaseProps {
|
|
|
55
58
|
setResizingClip: (v: ResizingClipState | null) => void;
|
|
56
59
|
setDraggedClip: (v: DraggedClipState | null) => void;
|
|
57
60
|
setSelectedElementId: (id: string | null) => void;
|
|
58
|
-
syncClipDragAutoScroll: (x: number, y: number) => void;
|
|
59
61
|
shiftClickClipRef: React.RefObject<{
|
|
60
62
|
element: TimelineElement;
|
|
61
63
|
anchorX: number;
|