@hyperframes/studio 0.7.57 → 0.7.59
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--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +27 -7
- package/dist/index.html +2 -2
- package/dist/index.js +18029 -11563
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +6 -6
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +101 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +55 -1
- package/src/hooks/timelineTrackVisibility.ts +31 -13
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +81 -162
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +182 -84
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +306 -155
- package/src/hooks/useTimelineEditing.ts +111 -101
- package/src/hooks/useTimelineEditingTypes.ts +4 -1
- package/src/hooks/useTimelineGroupEditing.ts +51 -5
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-DfmYkU44.js +0 -423
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -4,6 +4,7 @@ import { useMountEffect } from "../../hooks/useMountEffect";
|
|
|
4
4
|
import { getPinchTimelineZoomPercent } from "./timelineZoom";
|
|
5
5
|
import {
|
|
6
6
|
GUTTER,
|
|
7
|
+
TRACKS_LEFT_PAD,
|
|
7
8
|
getTimelinePlayheadLeft,
|
|
8
9
|
getTimelineScrollLeftForZoomTransition,
|
|
9
10
|
getTimelineScrollLeftForZoomAnchor,
|
|
@@ -73,7 +74,7 @@ export function useTimelinePlayhead({
|
|
|
73
74
|
const nextScrollLeft = getTimelineScrollLeftForZoomAnchor({
|
|
74
75
|
pointerX: scroll.clientWidth / 2,
|
|
75
76
|
currentScrollLeft: scroll.scrollLeft,
|
|
76
|
-
gutter: GUTTER,
|
|
77
|
+
gutter: GUTTER + TRACKS_LEFT_PAD,
|
|
77
78
|
currentPixelsPerSecond: prevPps,
|
|
78
79
|
nextPixelsPerSecond: pps,
|
|
79
80
|
duration: durationRef.current,
|
|
@@ -129,7 +130,7 @@ export function useTimelinePlayhead({
|
|
|
129
130
|
const el = scrollRef.current;
|
|
130
131
|
if (!el || effectiveDuration <= 0) return;
|
|
131
132
|
const rect = el.getBoundingClientRect();
|
|
132
|
-
const x = clientX - rect.left + el.scrollLeft - GUTTER;
|
|
133
|
+
const x = clientX - rect.left + el.scrollLeft - GUTTER - TRACKS_LEFT_PAD;
|
|
133
134
|
if (x < 0) return;
|
|
134
135
|
const time = Math.max(0, Math.min(effectiveDuration, x / pps));
|
|
135
136
|
liveTime.notify(time);
|
|
@@ -185,7 +186,7 @@ export function useTimelinePlayhead({
|
|
|
185
186
|
const nextScrollLeft = getTimelineScrollLeftForZoomAnchor({
|
|
186
187
|
pointerX: e.clientX - rect.left,
|
|
187
188
|
currentScrollLeft: scroll.scrollLeft,
|
|
188
|
-
gutter: GUTTER,
|
|
189
|
+
gutter: GUTTER + TRACKS_LEFT_PAD,
|
|
189
190
|
currentPixelsPerSecond: ppsRef.current,
|
|
190
191
|
nextPixelsPerSecond: nextPps,
|
|
191
192
|
duration: durationRef.current,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./timelineEditing";
|
|
8
8
|
import type { TimelineElement } from "../store/playerStore";
|
|
9
9
|
import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
10
|
-
import { GUTTER } from "./timelineLayout";
|
|
10
|
+
import { GUTTER, TRACKS_LEFT_PAD } from "./timelineLayout";
|
|
11
11
|
import {
|
|
12
12
|
computeMarqueeSelection,
|
|
13
13
|
getMarqueeRect,
|
|
@@ -228,7 +228,8 @@ export function useTimelineRangeSelection({
|
|
|
228
228
|
setShowPopover(false);
|
|
229
229
|
const rect = scrollRef.current?.getBoundingClientRect();
|
|
230
230
|
if (rect) {
|
|
231
|
-
const x =
|
|
231
|
+
const x =
|
|
232
|
+
e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER - TRACKS_LEFT_PAD;
|
|
232
233
|
const time = Math.max(0, x / pps);
|
|
233
234
|
rangeAnchorTime.current = time;
|
|
234
235
|
setRangeSelection({ start: time, end: time, anchorX: e.clientX, anchorY: e.clientY });
|
|
@@ -285,7 +286,7 @@ export function useTimelineRangeSelection({
|
|
|
285
286
|
const el = scrollRef.current;
|
|
286
287
|
if (el) {
|
|
287
288
|
const rect = el.getBoundingClientRect();
|
|
288
|
-
const x = clientX - rect.left + el.scrollLeft - GUTTER;
|
|
289
|
+
const x = clientX - rect.left + el.scrollLeft - GUTTER - TRACKS_LEFT_PAD;
|
|
289
290
|
if (x >= 0) {
|
|
290
291
|
const dur = el.scrollWidth / pps;
|
|
291
292
|
liveTime.notify(Math.max(0, Math.min(dur, x / pps)));
|
|
@@ -309,7 +310,8 @@ export function useTimelineRangeSelection({
|
|
|
309
310
|
if (isRangeSelecting.current) {
|
|
310
311
|
const rect = scrollRef.current?.getBoundingClientRect();
|
|
311
312
|
if (rect) {
|
|
312
|
-
const x =
|
|
313
|
+
const x =
|
|
314
|
+
e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER - TRACKS_LEFT_PAD;
|
|
313
315
|
setRangeSelection((prev) =>
|
|
314
316
|
prev
|
|
315
317
|
? { ...prev, end: Math.max(0, x / pps), anchorX: e.clientX, anchorY: e.clientY }
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
|
2
|
+
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
|
+
import { shouldShowTimelineShortcutHint } from "./timelineLayout";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The timeline scroll container's viewport plumbing — extracted verbatim from
|
|
7
|
+
* Timeline.tsx (600-line studio cap): the ResizeObserver-backed viewport width,
|
|
8
|
+
* the rAF-throttled shortcut-hint visibility sync, and the callback ref that
|
|
9
|
+
* wires both to the scroll element. `resyncShortcutHintOn` re-checks the hint
|
|
10
|
+
* whenever any of its values change (timeline readiness / element count /
|
|
11
|
+
* canvas height), matching the original effect.
|
|
12
|
+
*/
|
|
13
|
+
export function useTimelineScrollViewport(
|
|
14
|
+
scrollRef: RefObject<HTMLDivElement | null>,
|
|
15
|
+
resyncShortcutHintOn: ReadonlyArray<unknown>,
|
|
16
|
+
): {
|
|
17
|
+
viewportWidth: number;
|
|
18
|
+
showShortcutHint: boolean;
|
|
19
|
+
setScrollRef: (el: HTMLDivElement | null) => void;
|
|
20
|
+
} {
|
|
21
|
+
const [viewportWidth, setViewportWidth] = useState(0);
|
|
22
|
+
const [showShortcutHint, setShowShortcutHint] = useState(true);
|
|
23
|
+
const roRef = useRef<ResizeObserver | null>(null);
|
|
24
|
+
const shortcutHintRafRef = useRef(0);
|
|
25
|
+
|
|
26
|
+
const syncShortcutHintVisibility = useCallback(() => {
|
|
27
|
+
const scroll = scrollRef.current;
|
|
28
|
+
setShowShortcutHint(
|
|
29
|
+
scroll ? shouldShowTimelineShortcutHint(scroll.scrollHeight, scroll.clientHeight) : true,
|
|
30
|
+
);
|
|
31
|
+
}, [scrollRef]);
|
|
32
|
+
|
|
33
|
+
const scheduleShortcutHintVisibilitySync = useCallback(() => {
|
|
34
|
+
if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
|
|
35
|
+
shortcutHintRafRef.current = requestAnimationFrame(() => {
|
|
36
|
+
shortcutHintRafRef.current = 0;
|
|
37
|
+
syncShortcutHintVisibility();
|
|
38
|
+
});
|
|
39
|
+
}, [syncShortcutHintVisibility]);
|
|
40
|
+
|
|
41
|
+
const setScrollRef = useCallback(
|
|
42
|
+
(el: HTMLDivElement | null) => {
|
|
43
|
+
if (roRef.current) {
|
|
44
|
+
roRef.current.disconnect();
|
|
45
|
+
roRef.current = null;
|
|
46
|
+
}
|
|
47
|
+
scrollRef.current = el;
|
|
48
|
+
if (!el) return;
|
|
49
|
+
|
|
50
|
+
const syncScrollViewport = () => {
|
|
51
|
+
setViewportWidth(el.clientWidth);
|
|
52
|
+
scheduleShortcutHintVisibilitySync();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
syncScrollViewport();
|
|
56
|
+
roRef.current = new ResizeObserver(syncScrollViewport);
|
|
57
|
+
roRef.current.observe(el);
|
|
58
|
+
},
|
|
59
|
+
[scrollRef, scheduleShortcutHintVisibilitySync],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
useMountEffect(() => () => {
|
|
63
|
+
roRef.current?.disconnect();
|
|
64
|
+
if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
syncShortcutHintVisibility();
|
|
69
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
|
+
}, [syncShortcutHintVisibility, ...resyncShortcutHintOn]);
|
|
71
|
+
|
|
72
|
+
return { viewportWidth, showShortcutHint, setScrollRef };
|
|
73
|
+
}
|
|
@@ -37,7 +37,7 @@ export function useTimelineStackingSync({ expandedElementsRef }: UseTimelineStac
|
|
|
37
37
|
id: el.domId ?? el.id,
|
|
38
38
|
selector: el.selector,
|
|
39
39
|
selectorIndex: el.selectorIndex,
|
|
40
|
-
sourceFile: el.sourceFile,
|
|
40
|
+
sourceFile: el.sourceFile ?? zSyncActiveCompPath ?? "index.html",
|
|
41
41
|
},
|
|
42
42
|
zSyncActiveCompPath,
|
|
43
43
|
);
|
|
@@ -61,7 +61,7 @@ export function useTimelineStackingSync({ expandedElementsRef }: UseTimelineStac
|
|
|
61
61
|
|
|
62
62
|
const applyStackingPatches = useCallback(
|
|
63
63
|
(patches: StackingPatch[], coalesceKey?: string) => {
|
|
64
|
-
if (!handleDomZIndexReorderCommit) return;
|
|
64
|
+
if (!handleDomZIndexReorderCommit) return Promise.resolve();
|
|
65
65
|
const entries = patches.flatMap((p) => {
|
|
66
66
|
const el = expandedElementsRef.current.find((e) => (e.key ?? e.id) === p.key);
|
|
67
67
|
const node = el && resolveIframeElement(el);
|
|
@@ -82,7 +82,9 @@ export function useTimelineStackingSync({ expandedElementsRef }: UseTimelineStac
|
|
|
82
82
|
});
|
|
83
83
|
// Forward the drag-commit's shared coalesce key so the z-reorder history
|
|
84
84
|
// entry merges with the lane change's move entry into one undo step.
|
|
85
|
-
|
|
85
|
+
return entries.length
|
|
86
|
+
? handleDomZIndexReorderCommit(entries, coalesceKey).then(() => undefined)
|
|
87
|
+
: Promise.resolve();
|
|
86
88
|
},
|
|
87
89
|
[handleDomZIndexReorderCommit, resolveIframeElement, zSyncActiveCompPath, expandedElementsRef],
|
|
88
90
|
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
+
import { getTrackStyle, type TrackVisualStyle } from "./timelineIcons";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-render track derivations Timeline.tsx feeds the canvas/lanes: the lane →
|
|
7
|
+
* clip grouping (`tracks`, ascending), per-lane visual styles, the ascending
|
|
8
|
+
* `trackOrder`, and the z-override badge set. Extracted from Timeline.tsx as a
|
|
9
|
+
* cohesive unit (600-line studio cap); each memo keys on the expanded display
|
|
10
|
+
* element set exactly as before.
|
|
11
|
+
*/
|
|
12
|
+
export function useTimelineTrackDerivations(expandedElements: TimelineElement[]): {
|
|
13
|
+
tracks: [number, TimelineElement[]][];
|
|
14
|
+
trackStyles: Map<number, TrackVisualStyle>;
|
|
15
|
+
trackOrder: number[];
|
|
16
|
+
} {
|
|
17
|
+
const tracks = useMemo(() => {
|
|
18
|
+
const map = new Map<number, TimelineElement[]>();
|
|
19
|
+
for (const el of expandedElements) {
|
|
20
|
+
const list = map.get(el.track) ?? [];
|
|
21
|
+
list.push(el);
|
|
22
|
+
map.set(el.track, list);
|
|
23
|
+
}
|
|
24
|
+
return Array.from(map.entries()).sort(([a], [b]) => a - b);
|
|
25
|
+
}, [expandedElements]);
|
|
26
|
+
|
|
27
|
+
const trackStyles = useMemo(() => {
|
|
28
|
+
const map = new Map<number, TrackVisualStyle>();
|
|
29
|
+
for (const [trackNum, els] of tracks) {
|
|
30
|
+
map.set(trackNum, getTrackStyle(els[0]?.tag ?? ""));
|
|
31
|
+
}
|
|
32
|
+
return map;
|
|
33
|
+
}, [tracks]);
|
|
34
|
+
|
|
35
|
+
const trackOrder = useMemo(() => tracks.map(([trackNum]) => trackNum), [tracks]);
|
|
36
|
+
|
|
37
|
+
return { tracks, trackStyles, trackOrder };
|
|
38
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState, type MutableRefObject } from "react";
|
|
2
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
3
|
+
import type { DragCommitDeps } from "./timelineClipDragCommit";
|
|
4
|
+
import {
|
|
5
|
+
laneGapFloor,
|
|
6
|
+
resolveAllGapIntervals,
|
|
7
|
+
resolveAllTrackGaps,
|
|
8
|
+
resolveCloseGapShifts,
|
|
9
|
+
resolveTrackGapAt,
|
|
10
|
+
type TrackGapInterval,
|
|
11
|
+
} from "./timelineGaps";
|
|
12
|
+
import {
|
|
13
|
+
canShiftTrackGapClips,
|
|
14
|
+
commitCloseAllTrackGaps,
|
|
15
|
+
commitCloseTrackGap,
|
|
16
|
+
} from "./timelineGapCommit";
|
|
17
|
+
|
|
18
|
+
/** Right-click anchor on EMPTY lane space: pointer position + clicked lane/time. */
|
|
19
|
+
interface TrackGapMenuAnchor {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
track: number;
|
|
23
|
+
time: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Gap strips to paint on one lane while a menu row is hovered. */
|
|
27
|
+
export interface TrackGapHighlight {
|
|
28
|
+
track: number;
|
|
29
|
+
intervals: TrackGapInterval[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Track-gap context menu (right-click on empty lane space) — state, the
|
|
34
|
+
* derived menu model, and the two commit actions. Extracted from Timeline.tsx
|
|
35
|
+
* as a cohesive unit (600-line studio cap); behavior identical.
|
|
36
|
+
*
|
|
37
|
+
* Only the ANCHOR (and the hovered row) is state; the menu model (gap under
|
|
38
|
+
* the pointer, compaction, movability) derives from live `tracks` so an open
|
|
39
|
+
* menu reflects concurrent edits. `gapHighlight` — the strips TimelineCanvas
|
|
40
|
+
* paints while "Close gap" / "Close all gaps" is hovered — derives the same
|
|
41
|
+
* way. Commits are ONE atomic batch each via the existing move-persist
|
|
42
|
+
* pipeline (see timelineGapCommit.ts).
|
|
43
|
+
*/
|
|
44
|
+
export function useTrackGapMenu({
|
|
45
|
+
tracks,
|
|
46
|
+
expandedElementsRef,
|
|
47
|
+
trackOrderRef,
|
|
48
|
+
onMoveElement,
|
|
49
|
+
onMoveElements,
|
|
50
|
+
}: {
|
|
51
|
+
tracks: [number, TimelineElement[]][];
|
|
52
|
+
expandedElementsRef: MutableRefObject<TimelineElement[]>;
|
|
53
|
+
trackOrderRef: MutableRefObject<number[]>;
|
|
54
|
+
onMoveElement: DragCommitDeps["onMoveElement"];
|
|
55
|
+
onMoveElements: DragCommitDeps["onMoveElements"];
|
|
56
|
+
}) {
|
|
57
|
+
const updateElement = usePlayerStore((s) => s.updateElement);
|
|
58
|
+
const [gapContextMenu, setGapContextMenu] = useState<TrackGapMenuAnchor | null>(null);
|
|
59
|
+
const [hoveredGapAction, setHoveredGapAction] = useState<"close-gap" | "close-all" | null>(null);
|
|
60
|
+
|
|
61
|
+
const gapMenuLaneElements = useMemo(
|
|
62
|
+
() => (gapContextMenu ? (tracks.find(([t]) => t === gapContextMenu.track)?.[1] ?? []) : null),
|
|
63
|
+
[gapContextMenu, tracks],
|
|
64
|
+
);
|
|
65
|
+
const gapMenuModel = useMemo(() => {
|
|
66
|
+
if (!gapContextMenu || !gapMenuLaneElements) return null;
|
|
67
|
+
const floor = laneGapFloor(gapMenuLaneElements);
|
|
68
|
+
const gap = resolveTrackGapAt(gapMenuLaneElements, gapContextMenu.time, undefined, floor);
|
|
69
|
+
const allShifts = resolveAllTrackGaps(gapMenuLaneElements, undefined, floor);
|
|
70
|
+
return {
|
|
71
|
+
x: gapContextMenu.x,
|
|
72
|
+
y: gapContextMenu.y,
|
|
73
|
+
gapWidth: gap ? gap.gapEnd - gap.gapStart : null,
|
|
74
|
+
canCloseGap:
|
|
75
|
+
gap != null &&
|
|
76
|
+
canShiftTrackGapClips(gapMenuLaneElements, resolveCloseGapShifts(gapMenuLaneElements, gap)),
|
|
77
|
+
hasAnyGaps: allShifts.length > 0,
|
|
78
|
+
canCloseAllGaps:
|
|
79
|
+
allShifts.length > 0 && canShiftTrackGapClips(gapMenuLaneElements, allShifts),
|
|
80
|
+
};
|
|
81
|
+
}, [gapContextMenu, gapMenuLaneElements]);
|
|
82
|
+
|
|
83
|
+
// The strips to paint while a menu row is hovered: the one gap under the
|
|
84
|
+
// pointer for "Close gap", every current gap (leading included) for
|
|
85
|
+
// "Close all gaps". Null when nothing is hovered / nothing would close.
|
|
86
|
+
const gapHighlight = useMemo<TrackGapHighlight | null>(() => {
|
|
87
|
+
if (!gapContextMenu || !gapMenuLaneElements || !hoveredGapAction) return null;
|
|
88
|
+
const floor = laneGapFloor(gapMenuLaneElements);
|
|
89
|
+
if (hoveredGapAction === "close-gap") {
|
|
90
|
+
const gap = resolveTrackGapAt(gapMenuLaneElements, gapContextMenu.time, undefined, floor);
|
|
91
|
+
if (!gap) return null;
|
|
92
|
+
return {
|
|
93
|
+
track: gapContextMenu.track,
|
|
94
|
+
intervals: [{ start: gap.gapStart, end: gap.gapEnd }],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const intervals = resolveAllGapIntervals(gapMenuLaneElements, undefined, floor);
|
|
98
|
+
return intervals.length > 0 ? { track: gapContextMenu.track, intervals } : null;
|
|
99
|
+
}, [gapContextMenu, gapMenuLaneElements, hoveredGapAction]);
|
|
100
|
+
|
|
101
|
+
const closeTrackGap = useCallback(() => {
|
|
102
|
+
if (!gapContextMenu || !gapMenuLaneElements) return;
|
|
103
|
+
commitCloseTrackGap(gapMenuLaneElements, gapContextMenu.time, {
|
|
104
|
+
elements: expandedElementsRef.current,
|
|
105
|
+
trackOrder: trackOrderRef.current,
|
|
106
|
+
updateElement,
|
|
107
|
+
onMoveElement,
|
|
108
|
+
onMoveElements,
|
|
109
|
+
});
|
|
110
|
+
}, [
|
|
111
|
+
gapContextMenu,
|
|
112
|
+
gapMenuLaneElements,
|
|
113
|
+
expandedElementsRef,
|
|
114
|
+
trackOrderRef,
|
|
115
|
+
updateElement,
|
|
116
|
+
onMoveElement,
|
|
117
|
+
onMoveElements,
|
|
118
|
+
]);
|
|
119
|
+
const closeAllTrackGaps = useCallback(() => {
|
|
120
|
+
if (!gapMenuLaneElements) return;
|
|
121
|
+
commitCloseAllTrackGaps(gapMenuLaneElements, {
|
|
122
|
+
elements: expandedElementsRef.current,
|
|
123
|
+
trackOrder: trackOrderRef.current,
|
|
124
|
+
updateElement,
|
|
125
|
+
onMoveElement,
|
|
126
|
+
onMoveElements,
|
|
127
|
+
});
|
|
128
|
+
}, [
|
|
129
|
+
gapMenuLaneElements,
|
|
130
|
+
expandedElementsRef,
|
|
131
|
+
trackOrderRef,
|
|
132
|
+
updateElement,
|
|
133
|
+
onMoveElement,
|
|
134
|
+
onMoveElements,
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const openGapMenu = useCallback((anchor: TrackGapMenuAnchor) => {
|
|
138
|
+
setHoveredGapAction(null);
|
|
139
|
+
setGapContextMenu(anchor);
|
|
140
|
+
}, []);
|
|
141
|
+
const dismissGapMenu = useCallback(() => {
|
|
142
|
+
setHoveredGapAction(null);
|
|
143
|
+
setGapContextMenu(null);
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
gapMenuModel,
|
|
148
|
+
gapHighlight,
|
|
149
|
+
setHoveredGapAction,
|
|
150
|
+
openGapMenu,
|
|
151
|
+
dismissGapMenu,
|
|
152
|
+
closeTrackGap,
|
|
153
|
+
closeAllTrackGaps,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -152,10 +152,34 @@ describe("buildExpandedElements", () => {
|
|
|
152
152
|
["pill-3", "group-1"],
|
|
153
153
|
]);
|
|
154
154
|
const domClipChildren = [
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
{
|
|
156
|
+
id: "group-1",
|
|
157
|
+
parentId: "scene-host",
|
|
158
|
+
hostId: "scene-host",
|
|
159
|
+
label: "Group 1",
|
|
160
|
+
stackingContextId: "css:0",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "pill-1",
|
|
164
|
+
parentId: "group-1",
|
|
165
|
+
hostId: "scene-host",
|
|
166
|
+
label: "pill-1",
|
|
167
|
+
stackingContextId: "css:0.0",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "pill-2",
|
|
171
|
+
parentId: "group-1",
|
|
172
|
+
hostId: "scene-host",
|
|
173
|
+
label: "pill-2",
|
|
174
|
+
stackingContextId: "css:0.1",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "pill-3",
|
|
178
|
+
parentId: "group-1",
|
|
179
|
+
hostId: "scene-host",
|
|
180
|
+
label: "pill-3",
|
|
181
|
+
stackingContextId: "css:0.1",
|
|
182
|
+
},
|
|
159
183
|
];
|
|
160
184
|
|
|
161
185
|
// Expanding pill-3's siblings: topLevel scene-host, immediate parent group-1.
|
|
@@ -173,6 +197,7 @@ describe("buildExpandedElements", () => {
|
|
|
173
197
|
expect(pills[0]!.start).toBe(5);
|
|
174
198
|
expect(pills[0]!.duration).toBe(6);
|
|
175
199
|
expect(pills[0]!.sourceFile).toBe("scene.html");
|
|
200
|
+
expect(pills.map((pill) => pill.stackingContextId)).toEqual(["css:0.0", "css:0.1", "css:0.1"]);
|
|
176
201
|
// The host row is replaced by its children.
|
|
177
202
|
expect(out.some((e) => e.domId === "scene-host")).toBe(false);
|
|
178
203
|
});
|
|
@@ -273,3 +298,41 @@ describe("resolveTimelineExpansionRawId", () => {
|
|
|
273
298
|
).toBe("caption");
|
|
274
299
|
});
|
|
275
300
|
});
|
|
301
|
+
|
|
302
|
+
describe("buildExpandedElements — collision-free synthetic rows (cross-file lane safety)", () => {
|
|
303
|
+
it("expanded children NEVER share a display track with an unrelated top-level clip", () => {
|
|
304
|
+
// The Deepwork regression: host on track 0 with two children used to put
|
|
305
|
+
// child #2 on integer track 1 — the same lane as index.html#foreign. Lane
|
|
306
|
+
// grouping merges purely by track number, so a gap-close on that "one"
|
|
307
|
+
// lane batch-persisted a foreign file's clip.
|
|
308
|
+
const elements = [
|
|
309
|
+
el({ id: "host", start: 0, duration: 20, track: 0, compositionSrc: "scene.html" }),
|
|
310
|
+
el({ id: "foreign", start: 20, duration: 5, track: 1 }),
|
|
311
|
+
];
|
|
312
|
+
const manifest = [
|
|
313
|
+
clip({ id: "host", start: 0, duration: 20, compositionSrc: "scene.html" }),
|
|
314
|
+
clip({ id: "c1", start: 0, duration: 5 }),
|
|
315
|
+
clip({ id: "c2", start: 10, duration: 5 }),
|
|
316
|
+
];
|
|
317
|
+
const parentMap = new Map([
|
|
318
|
+
["c1", "host"],
|
|
319
|
+
["c2", "host"],
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
const out = buildExpandedElements(elements, manifest, parentMap, "host", "host");
|
|
323
|
+
const foreign = out.find((e) => e.id === "foreign")!;
|
|
324
|
+
const children = out.filter((e) => e.domId === "c1" || e.domId === "c2");
|
|
325
|
+
expect(children).toHaveLength(2);
|
|
326
|
+
for (const child of children) {
|
|
327
|
+
// No lane sharing with the foreign clip…
|
|
328
|
+
expect(child.track).not.toBe(foreign.track);
|
|
329
|
+
// …and structurally impossible to collide with ANY normalized (integer)
|
|
330
|
+
// lane: synthetic rows are strict fractions under the host's lane.
|
|
331
|
+
expect(Number.isInteger(child.track)).toBe(false);
|
|
332
|
+
expect(child.track).toBeGreaterThan(0);
|
|
333
|
+
expect(child.track).toBeLessThan(1);
|
|
334
|
+
}
|
|
335
|
+
// Distinct ordered rows per child.
|
|
336
|
+
expect(children[0].track).not.toBe(children[1].track);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -168,7 +168,17 @@ function buildChildElements(
|
|
|
168
168
|
// file's coordinate space) and the runtime-computed `stackingContextId`
|
|
169
169
|
// must survive verbatim — lane persists and z-sync read them, they are
|
|
170
170
|
// never reconstructed from display lanes.
|
|
171
|
-
|
|
171
|
+
//
|
|
172
|
+
// COLLISION-FREE synthetic rows: the old `display.track + index` scheme
|
|
173
|
+
// could equal a REAL clip's integer lane (host on track 0 with two
|
|
174
|
+
// children puts child #2 on track 1 — where an unrelated top-level clip
|
|
175
|
+
// may live). Lane grouping merges purely by track number, so that
|
|
176
|
+
// collision fused clips from DIFFERENT source files into one display
|
|
177
|
+
// lane, and lane-scoped actions (gap close) then batch-persisted foreign
|
|
178
|
+
// clips. Fractions strictly between the host's lane and the next integer
|
|
179
|
+
// can never equal a normalized (integer) lane, while still rendering the
|
|
180
|
+
// children as their own ordered rows directly under the host.
|
|
181
|
+
track: display.track + (result.length + 1) / (siblings.length + 2),
|
|
172
182
|
authoredTrack: base.authoredTrack,
|
|
173
183
|
stackingContextId: base.stackingContextId,
|
|
174
184
|
expandedParentStart: editBasis.start,
|
|
@@ -205,6 +215,7 @@ function domSiblingClips(
|
|
|
205
215
|
parentCompositionId: host.id ?? null,
|
|
206
216
|
compositionSrc: host.compositionSrc ?? null,
|
|
207
217
|
assetUrl: null,
|
|
218
|
+
stackingContextId: c.stackingContextId,
|
|
208
219
|
}),
|
|
209
220
|
);
|
|
210
221
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { useCallback } from "react";
|
|
12
12
|
import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
13
13
|
import type { TimelineElement, DomClipChild } from "../store/playerStore";
|
|
14
|
+
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
15
|
import type { PlaybackAdapter, ClipManifestClip, IframeWindow } from "../lib/playbackTypes";
|
|
15
16
|
import {
|
|
16
17
|
parseTimelineFromDOM,
|
|
@@ -199,6 +200,7 @@ export function useTimelineSyncCallbacks({
|
|
|
199
200
|
parentId,
|
|
200
201
|
hostId,
|
|
201
202
|
label: isGroup ? child.getAttribute("data-hf-group") || child.id : child.id,
|
|
203
|
+
stackingContextId: resolveCssStackingContextId(child),
|
|
202
204
|
});
|
|
203
205
|
parentMap.set(child.id, parentId);
|
|
204
206
|
if (isGroup) collect(child, child.id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import { getElementZIndex, resolveContextOrder } from "./layerOrdering";
|
|
5
5
|
|
|
6
6
|
function makeElement(zIndex?: string): HTMLElement {
|
|
7
7
|
const element = document.createElement("div");
|
|
@@ -31,20 +31,6 @@ describe("getElementZIndex", () => {
|
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
describe("computeReorderZValues", () => {
|
|
35
|
-
it("preserves distinct existing z-index values and remaps them onto the new order", () => {
|
|
36
|
-
expect(computeReorderZValues([1, 8, 3], 0, 2)).toEqual([8, 3, 1]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("renumbers an all-tied group to descending contiguous z-index values", () => {
|
|
40
|
-
expect(computeReorderZValues([0, 0, 0], 2, 0)).toEqual([3, 2, 1]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("renumbers the whole group when any existing z-index values are tied", () => {
|
|
44
|
-
expect(computeReorderZValues([5, 5, 1], 2, 0)).toEqual([3, 2, 1]);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
34
|
describe("resolveContextOrder", () => {
|
|
49
35
|
it("sorts a flat sibling group by z-index descending then original order", () => {
|
|
50
36
|
const ordered = resolveContextOrder([
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readLayerRevealPriorZ } from "./timelineElementHelpers";
|
|
2
|
+
|
|
1
3
|
export interface StackingContextDescriptor {
|
|
2
4
|
parentCompositionId: string | null;
|
|
3
5
|
compositionAncestors: readonly string[];
|
|
@@ -11,6 +13,9 @@ export interface ContextOrderItem extends StackingContextDescriptor {
|
|
|
11
13
|
// fallow-ignore-next-line complexity
|
|
12
14
|
export function getElementZIndex(element: HTMLElement): number {
|
|
13
15
|
try {
|
|
16
|
+
// An active Layers-panel reveal lift reports the element's TRUE z.
|
|
17
|
+
const prior = readLayerRevealPriorZ(element);
|
|
18
|
+
if (prior != null) return prior;
|
|
14
19
|
const inline = element.style?.zIndex;
|
|
15
20
|
if (inline && inline !== "auto") {
|
|
16
21
|
const parsed = parseInt(inline, 10);
|
|
@@ -27,34 +32,6 @@ export function getElementZIndex(element: HTMLElement): number {
|
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
// fallow-ignore-next-line complexity
|
|
31
|
-
export function hasExplicitZIndex(element: HTMLElement): boolean {
|
|
32
|
-
try {
|
|
33
|
-
const inline = element.style?.zIndex;
|
|
34
|
-
if (inline) return inline !== "auto";
|
|
35
|
-
const win = element.ownerDocument?.defaultView;
|
|
36
|
-
if (!win) return false;
|
|
37
|
-
const value = win.getComputedStyle(element).zIndex;
|
|
38
|
-
return value !== "auto" && value !== "";
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function computeReorderZValues(
|
|
45
|
-
existingValues: readonly number[],
|
|
46
|
-
fromIndex: number,
|
|
47
|
-
toIndex: number,
|
|
48
|
-
): number[] {
|
|
49
|
-
const reordered = [...existingValues];
|
|
50
|
-
const [moved] = reordered.splice(fromIndex, 1);
|
|
51
|
-
reordered.splice(toIndex, 0, moved);
|
|
52
|
-
|
|
53
|
-
const sorted = [...existingValues].sort((a, b) => b - a);
|
|
54
|
-
const hasDupes = sorted.some((v, i) => i > 0 && v === sorted[i - 1]);
|
|
55
|
-
return hasDupes ? reordered.map((_, i) => reordered.length - i) : sorted;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
35
|
export function resolveStackingContextKey(item: StackingContextDescriptor): string {
|
|
59
36
|
return item.stackingContextId ?? item.parentCompositionId ?? item.compositionAncestors[0] ?? "";
|
|
60
37
|
}
|
|
@@ -110,7 +110,79 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
|
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
describe("createTimelineElementFromManifestClip — source-scoped selector identity", () => {
|
|
114
|
+
it("ignores an index.html duplicate when indexing a scene.html selector", () => {
|
|
115
|
+
const doc = makeDoc(`
|
|
116
|
+
<div data-composition-id="root" data-composition-file="index.html">
|
|
117
|
+
<div class="sub"></div>
|
|
118
|
+
<div data-composition-id="scene" data-composition-file="scene.html">
|
|
119
|
+
<div class="sub"></div>
|
|
120
|
+
<div class="sub" data-target></div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
`);
|
|
124
|
+
const target = doc.querySelector("[data-target]");
|
|
125
|
+
if (!target) throw new Error("missing target");
|
|
126
|
+
|
|
127
|
+
const element = createTimelineElementFromManifestClip({
|
|
128
|
+
clip: {
|
|
129
|
+
id: null,
|
|
130
|
+
label: "Sub",
|
|
131
|
+
kind: "element",
|
|
132
|
+
tagName: "div",
|
|
133
|
+
start: 0,
|
|
134
|
+
duration: 5,
|
|
135
|
+
track: 0,
|
|
136
|
+
compositionId: null,
|
|
137
|
+
parentCompositionId: null,
|
|
138
|
+
compositionSrc: null,
|
|
139
|
+
assetUrl: null,
|
|
140
|
+
},
|
|
141
|
+
fallbackIndex: 0,
|
|
142
|
+
doc,
|
|
143
|
+
hostEl: target,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(element.sourceFile).toBe("scene.html");
|
|
147
|
+
expect(element.selectorIndex).toBe(1);
|
|
148
|
+
expect(element.key).toBe("scene.html:.sub:1");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
113
152
|
describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
|
|
153
|
+
it("uses the runtime root paint scope for implicit siblings of manifest clips", () => {
|
|
154
|
+
const doc = makeDoc(`
|
|
155
|
+
<div data-composition-id="root">
|
|
156
|
+
<div id="timed" data-start="0" data-duration="5"></div>
|
|
157
|
+
<div id="implicit"></div>
|
|
158
|
+
</div>
|
|
159
|
+
`);
|
|
160
|
+
const timedHost = doc.getElementById("timed");
|
|
161
|
+
const timed = createTimelineElementFromManifestClip({
|
|
162
|
+
clip: {
|
|
163
|
+
id: "timed",
|
|
164
|
+
label: "Timed",
|
|
165
|
+
start: 0,
|
|
166
|
+
duration: 5,
|
|
167
|
+
track: 0,
|
|
168
|
+
stackingContextId: "css:root",
|
|
169
|
+
kind: "element",
|
|
170
|
+
tagName: "div",
|
|
171
|
+
compositionId: null,
|
|
172
|
+
parentCompositionId: null,
|
|
173
|
+
compositionSrc: null,
|
|
174
|
+
assetUrl: null,
|
|
175
|
+
},
|
|
176
|
+
fallbackIndex: 0,
|
|
177
|
+
doc,
|
|
178
|
+
hostEl: timedHost,
|
|
179
|
+
});
|
|
180
|
+
const implicit = createImplicitTimelineLayersFromDOM(doc, 5, [timed])[0];
|
|
181
|
+
|
|
182
|
+
expect(implicit?.stackingContextId).toBe("css:root");
|
|
183
|
+
expect(implicit?.stackingContextId).toBe(timed.stackingContextId);
|
|
184
|
+
});
|
|
185
|
+
|
|
114
186
|
it("harvests hfId from an implicit layer child that has data-hf-id", () => {
|
|
115
187
|
const doc = makeDoc(`
|
|
116
188
|
<div data-composition-id="root">
|