@hyperframes/studio 0.7.55 → 0.7.57
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 +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -21,6 +21,15 @@ interface PreviewLocalPointer {
|
|
|
21
21
|
// should remain canvas-selectable.
|
|
22
22
|
const FULL_BLEED_RATIO = 0.95;
|
|
23
23
|
|
|
24
|
+
// Media leaves (a hero/background video, a full-bleed image, an <svg>/<canvas>
|
|
25
|
+
// backdrop) ARE the content a user clicks — they must stay canvas-selectable even
|
|
26
|
+
// at full-bleed. Only empty containers (scene wrappers, layout backdrops) get
|
|
27
|
+
// excluded. Without this, a full-bleed <video> is skipped and the click lands on
|
|
28
|
+
// whatever sits behind it — the reported "can't select videos / selects the layer
|
|
29
|
+
// behind" bug (and the "needs a second click" symptom, where the first click
|
|
30
|
+
// resolves through the video to nothing and only the hover fallback recovers).
|
|
31
|
+
const FULL_BLEED_SELECTABLE_MEDIA_TAGS = new Set(["video", "img", "canvas", "svg"]);
|
|
32
|
+
|
|
24
33
|
export function coversComposition(
|
|
25
34
|
elRect: { width: number; height: number },
|
|
26
35
|
viewport: DomEditViewport,
|
|
@@ -33,6 +42,7 @@ export function coversComposition(
|
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
function isFullBleedTarget(el: HTMLElement, viewport: DomEditViewport): boolean {
|
|
45
|
+
if (FULL_BLEED_SELECTABLE_MEDIA_TAGS.has(el.tagName.toLowerCase())) return false;
|
|
36
46
|
return coversComposition(el.getBoundingClientRect(), viewport);
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -42,7 +42,10 @@ function getSessionProperties(): EventProperties {
|
|
|
42
42
|
viewport_width: window.innerWidth,
|
|
43
43
|
viewport_height: window.innerHeight,
|
|
44
44
|
user_agent: navigator.userAgent,
|
|
45
|
-
|
|
45
|
+
// Route slug only — drop the query string, which carries the current
|
|
46
|
+
// selection (selId / selSelector are the user's own element ids/CSS
|
|
47
|
+
// selectors) and other view state we must not send to analytics.
|
|
48
|
+
url_hash: location.hash.replace(/#project\//, "").split("?")[0],
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -79,7 +79,6 @@ function renderStudioUrlStateHarness(
|
|
|
79
79
|
previewIframeRef: { current: null },
|
|
80
80
|
rightPanelTab: "renders",
|
|
81
81
|
rightCollapsed: true,
|
|
82
|
-
timelineVisible: true,
|
|
83
82
|
activeCompPathHydrated: true,
|
|
84
83
|
domEditSelection: null,
|
|
85
84
|
buildDomSelectionFromTarget: () => Promise.resolve(null),
|
|
@@ -1,13 +1,80 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
import {
|
|
3
4
|
buildTimelineFileDropPlacements,
|
|
4
5
|
buildTimelineAssetInsertHtml,
|
|
6
|
+
extendCompositionDurationIfNeeded,
|
|
7
|
+
fitTimelineAssetGeometry,
|
|
5
8
|
getTimelineAssetKind,
|
|
6
9
|
insertTimelineAssetIntoSource,
|
|
7
|
-
|
|
10
|
+
resolveTimelineAssetCompositionSize,
|
|
8
11
|
resolveTimelineAssetSrc,
|
|
12
|
+
setCompositionDurationToContent,
|
|
9
13
|
} from "./timelineAssetDrop";
|
|
10
14
|
|
|
15
|
+
describe("setCompositionDurationToContent", () => {
|
|
16
|
+
const src = (dur: number) =>
|
|
17
|
+
`<div id="root" data-composition-id="c" data-duration="${dur}">x</div>`;
|
|
18
|
+
|
|
19
|
+
it("shrinks the root duration to the content end", () => {
|
|
20
|
+
expect(setCompositionDurationToContent(src(20), 8)).toContain('data-duration="8"');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("grows the root duration to the content end", () => {
|
|
24
|
+
expect(setCompositionDurationToContent(src(5), 12)).toContain('data-duration="12"');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("is a no-op when content end is 0 (empty timeline keeps its declared length)", () => {
|
|
28
|
+
expect(setCompositionDurationToContent(src(12), 0)).toBe(src(12));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("is a no-op when already equal", () => {
|
|
32
|
+
expect(setCompositionDurationToContent(src(9), 9)).toBe(src(9));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Reviewer round-2 finding #3: attribute-order and single-quote variants that
|
|
36
|
+
// the old order-dependent, double-quotes-only regex silently ignored.
|
|
37
|
+
it("patches when data-duration precedes data-composition-id", () => {
|
|
38
|
+
const source = `<div data-duration="20" data-composition-id="c">x</div>`;
|
|
39
|
+
expect(setCompositionDurationToContent(source, 8)).toBe(
|
|
40
|
+
`<div data-duration="8" data-composition-id="c">x</div>`,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("patches single-quoted attributes and keeps the quote style", () => {
|
|
45
|
+
const source = `<div data-composition-id='c' data-duration='20'>x</div>`;
|
|
46
|
+
expect(setCompositionDurationToContent(source, 8)).toBe(
|
|
47
|
+
`<div data-composition-id='c' data-duration='8'>x</div>`,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("extendCompositionDurationIfNeeded", () => {
|
|
53
|
+
it("grows the root duration when a clip lands past the end", () => {
|
|
54
|
+
const source = `<div data-composition-id="c" data-duration="5">x</div>`;
|
|
55
|
+
expect(extendCompositionDurationIfNeeded(source, 8)).toBe(
|
|
56
|
+
`<div data-composition-id="c" data-duration="8">x</div>`,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("is a no-op when the required end fits within the current duration", () => {
|
|
61
|
+
const source = `<div data-composition-id="c" data-duration="10">x</div>`;
|
|
62
|
+
expect(extendCompositionDurationIfNeeded(source, 8)).toBe(source);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("grows even when the attribute order is swapped and quotes are single", () => {
|
|
66
|
+
const source = `<div data-duration='5' data-composition-id='c'>x</div>`;
|
|
67
|
+
expect(extendCompositionDurationIfNeeded(source, 8)).toBe(
|
|
68
|
+
`<div data-duration='8' data-composition-id='c'>x</div>`,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("is a no-op when there is no composition root", () => {
|
|
73
|
+
const source = `<div data-duration="5">x</div>`;
|
|
74
|
+
expect(extendCompositionDurationIfNeeded(source, 8)).toBe(source);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
11
78
|
describe("getTimelineAssetKind", () => {
|
|
12
79
|
it("detects image, video, and audio assets", () => {
|
|
13
80
|
expect(getTimelineAssetKind("assets/photo.png")).toBe("image");
|
|
@@ -16,12 +83,28 @@ describe("getTimelineAssetKind", () => {
|
|
|
16
83
|
expect(getTimelineAssetKind("assets/music.mp3")).toBe("audio");
|
|
17
84
|
expect(getTimelineAssetKind("assets/music.wav")).toBe("audio");
|
|
18
85
|
});
|
|
86
|
+
|
|
87
|
+
it("classifies svg as image", () => {
|
|
88
|
+
expect(getTimelineAssetKind("assets/logo.svg")).toBe("image");
|
|
89
|
+
expect(getTimelineAssetKind("assets/ICON.SVG")).toBe("image");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("classifies avif and webp as image", () => {
|
|
93
|
+
expect(getTimelineAssetKind("assets/photo.avif")).toBe("image");
|
|
94
|
+
expect(getTimelineAssetKind("assets/photo.webp")).toBe("image");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns null for unknown extensions", () => {
|
|
98
|
+
expect(getTimelineAssetKind("assets/data.json")).toBeNull();
|
|
99
|
+
expect(getTimelineAssetKind("assets/font.woff2")).toBeNull();
|
|
100
|
+
});
|
|
19
101
|
});
|
|
20
102
|
|
|
21
103
|
describe("buildTimelineAssetInsertHtml", () => {
|
|
22
104
|
it("builds an image clip with explicit timing and track", () => {
|
|
23
105
|
const html = buildTimelineAssetInsertHtml({
|
|
24
106
|
id: "photo_asset",
|
|
107
|
+
hfId: "hf-abc123",
|
|
25
108
|
assetPath: "assets/photo.png",
|
|
26
109
|
kind: "image",
|
|
27
110
|
start: 1.25,
|
|
@@ -40,6 +123,7 @@ describe("buildTimelineAssetInsertHtml", () => {
|
|
|
40
123
|
it("builds an audio clip without visual layout styles", () => {
|
|
41
124
|
const html = buildTimelineAssetInsertHtml({
|
|
42
125
|
id: "music_asset",
|
|
126
|
+
hfId: "hf-xyz789",
|
|
43
127
|
assetPath: "assets/music.wav",
|
|
44
128
|
kind: "audio",
|
|
45
129
|
start: 0.5,
|
|
@@ -52,15 +136,13 @@ describe("buildTimelineAssetInsertHtml", () => {
|
|
|
52
136
|
});
|
|
53
137
|
});
|
|
54
138
|
|
|
55
|
-
describe("
|
|
139
|
+
describe("resolveTimelineAssetCompositionSize", () => {
|
|
56
140
|
it("uses the target composition dimensions for visual media", () => {
|
|
57
141
|
expect(
|
|
58
|
-
|
|
142
|
+
resolveTimelineAssetCompositionSize(
|
|
59
143
|
`<div data-composition-id="main" data-width="330" data-height="228"></div>`,
|
|
60
144
|
),
|
|
61
145
|
).toEqual({
|
|
62
|
-
left: 0,
|
|
63
|
-
top: 0,
|
|
64
146
|
width: 330,
|
|
65
147
|
height: 228,
|
|
66
148
|
});
|
|
@@ -84,7 +166,9 @@ describe("buildTimelineFileDropPlacements", () => {
|
|
|
84
166
|
expect(buildTimelineFileDropPlacements({ start: 1.5, track: 2 }, [])).toEqual([]);
|
|
85
167
|
});
|
|
86
168
|
|
|
87
|
-
it("
|
|
169
|
+
it("spaces multiple files by duration and keeps every one on the dropped track", () => {
|
|
170
|
+
// A clip placed onto an occupied track stays there (overlap is allowed); it is
|
|
171
|
+
// NOT bumped to a new track — that produced surprise empty tracks for users.
|
|
88
172
|
expect(buildTimelineFileDropPlacements({ start: 1.5, track: 2 }, [1.2, 1.6, 1.1])).toEqual([
|
|
89
173
|
{ start: 1.5, track: 2 },
|
|
90
174
|
{ start: 2.7, track: 2 },
|
|
@@ -99,52 +183,6 @@ describe("buildTimelineFileDropPlacements", () => {
|
|
|
99
183
|
{ start: 7.7, track: 2 },
|
|
100
184
|
]);
|
|
101
185
|
});
|
|
102
|
-
|
|
103
|
-
it("moves the spaced sequence to a clear track when the dropped row is occupied", () => {
|
|
104
|
-
expect(
|
|
105
|
-
buildTimelineFileDropPlacements(
|
|
106
|
-
{ start: 1.5, track: 2 },
|
|
107
|
-
[1.2, 1.6, 1.1],
|
|
108
|
-
[
|
|
109
|
-
{ start: 0, duration: 8, track: 2 },
|
|
110
|
-
{ start: 0, duration: 4, track: 5 },
|
|
111
|
-
],
|
|
112
|
-
),
|
|
113
|
-
).toEqual([
|
|
114
|
-
{ start: 1.5, track: 6 },
|
|
115
|
-
{ start: 2.7, track: 6 },
|
|
116
|
-
{ start: 4.3, track: 6 },
|
|
117
|
-
]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("keeps a requested track above occupied rows when that track is clear", () => {
|
|
121
|
-
expect(
|
|
122
|
-
buildTimelineFileDropPlacements(
|
|
123
|
-
{ start: 1.5, track: 8 },
|
|
124
|
-
[1.2, 1.6],
|
|
125
|
-
[
|
|
126
|
-
{ start: 0, duration: 8, track: 2 },
|
|
127
|
-
{ start: 0, duration: 4, track: 5 },
|
|
128
|
-
],
|
|
129
|
-
),
|
|
130
|
-
).toEqual([
|
|
131
|
-
{ start: 1.5, track: 8 },
|
|
132
|
-
{ start: 2.7, track: 8 },
|
|
133
|
-
]);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("moves a default-track drop to a clear row when track 0 is occupied at time 0", () => {
|
|
137
|
-
expect(
|
|
138
|
-
buildTimelineFileDropPlacements(
|
|
139
|
-
{ start: 0, track: 0 },
|
|
140
|
-
[1.2, 1.6],
|
|
141
|
-
[{ start: 0, duration: 8, track: 0 }],
|
|
142
|
-
),
|
|
143
|
-
).toEqual([
|
|
144
|
-
{ start: 0, track: 1 },
|
|
145
|
-
{ start: 1.2, track: 1 },
|
|
146
|
-
]);
|
|
147
|
-
});
|
|
148
186
|
});
|
|
149
187
|
|
|
150
188
|
describe("insertTimelineAssetIntoSource", () => {
|
|
@@ -159,3 +197,57 @@ describe("insertTimelineAssetIntoSource", () => {
|
|
|
159
197
|
expect(html).toContain('<img id="photo_asset" data-start="0" data-duration="3" />');
|
|
160
198
|
});
|
|
161
199
|
});
|
|
200
|
+
|
|
201
|
+
describe("buildTimelineAssetInsertHtml markup quality", () => {
|
|
202
|
+
const base = {
|
|
203
|
+
id: "clip_1",
|
|
204
|
+
hfId: "hf-test-1",
|
|
205
|
+
assetPath: "assets/a.mp4",
|
|
206
|
+
start: 1,
|
|
207
|
+
duration: 4,
|
|
208
|
+
track: 2,
|
|
209
|
+
zIndex: 3,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
it("stamps data-hf-id on all kinds", () => {
|
|
213
|
+
for (const kind of ["image", "video", "audio"] as const) {
|
|
214
|
+
expect(buildTimelineAssetInsertHtml({ ...base, kind })).toContain('data-hf-id="hf-test-1"');
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("audio gets an explicit data-volume", () => {
|
|
219
|
+
expect(buildTimelineAssetInsertHtml({ ...base, kind: "audio" })).toContain('data-volume="1"');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("fitTimelineAssetGeometry", () => {
|
|
224
|
+
const comp = { width: 1920, height: 1080 };
|
|
225
|
+
|
|
226
|
+
it("centers a smaller-than-comp asset at natural size", () => {
|
|
227
|
+
expect(fitTimelineAssetGeometry({ width: 640, height: 360 }, comp)).toEqual({
|
|
228
|
+
left: 640,
|
|
229
|
+
top: 360,
|
|
230
|
+
width: 640,
|
|
231
|
+
height: 360,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("scales an oversized asset down to fit, preserving aspect, centered", () => {
|
|
236
|
+
// 4000x1000 → capped to 1920 wide → 1920x480, centered vertically
|
|
237
|
+
expect(fitTimelineAssetGeometry({ width: 4000, height: 1000 }, comp)).toEqual({
|
|
238
|
+
left: 0,
|
|
239
|
+
top: 300,
|
|
240
|
+
width: 1920,
|
|
241
|
+
height: 480,
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("falls back to full-frame when natural size is unknown", () => {
|
|
246
|
+
expect(fitTimelineAssetGeometry(null, comp)).toEqual({
|
|
247
|
+
left: 0,
|
|
248
|
+
top: 0,
|
|
249
|
+
width: 1920,
|
|
250
|
+
height: 1080,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AUDIO_EXT, IMAGE_EXT, VIDEO_EXT } from "./mediaTypes";
|
|
2
2
|
import { roundToCenti } from "./rounding";
|
|
3
3
|
import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns";
|
|
4
|
+
import { patchRootCompositionDuration, readRootCompositionDuration } from "./rootDuration";
|
|
4
5
|
|
|
5
6
|
export const TIMELINE_ASSET_MIME = "application/x-hyperframes-asset";
|
|
6
7
|
export const TIMELINE_BLOCK_MIME = "application/x-hyperframes-block";
|
|
@@ -48,56 +49,66 @@ export function resolveTimelineAssetSrc(targetPath: string, assetPath: string):
|
|
|
48
49
|
return relative || assetPath.split("/").pop() || assetPath;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Sequence one or more dropped files end-to-end starting at the drop point, all on
|
|
54
|
+
* the track the user dropped onto. The clip lands where the ghost showed it — we do
|
|
55
|
+
* NOT bump to a different track on overlap (that produced surprise "new tracks" and,
|
|
56
|
+
* because it jumped past high indices like a grain-overlay track, wild numbers).
|
|
57
|
+
* HyperFrames allows time-overlap on a track; the user can nudge if they want a gap.
|
|
58
|
+
*/
|
|
51
59
|
export function buildTimelineFileDropPlacements(
|
|
52
60
|
placement: { start: number; track: number },
|
|
53
61
|
durations: number[],
|
|
54
|
-
occupiedClips: Array<{ start: number; duration: number; track: number }> = [],
|
|
55
62
|
): Array<{ start: number; track: number }> {
|
|
56
63
|
let nextStart = roundToCenti(Math.max(0, placement.start));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
(end, duration) => roundToCenti(end + duration),
|
|
63
|
-
sequenceStart,
|
|
64
|
-
);
|
|
65
|
-
const overlapsDropTrack = occupiedClips.some((clip) => {
|
|
66
|
-
if (clip.track !== placement.track) return false;
|
|
67
|
-
const clipStart = Math.max(0, clip.start);
|
|
68
|
-
const clipEnd = clipStart + Math.max(0, clip.duration);
|
|
69
|
-
return sequenceStart < clipEnd && sequenceEnd > clipStart;
|
|
70
|
-
});
|
|
71
|
-
const track = overlapsDropTrack
|
|
72
|
-
? Math.max(placement.track, ...occupiedClips.map((clip) => clip.track)) + 1
|
|
73
|
-
: placement.track;
|
|
74
|
-
|
|
75
|
-
return resolvedDurations.map((duration) => {
|
|
64
|
+
return durations.map((rawDuration) => {
|
|
65
|
+
const duration =
|
|
66
|
+
Number.isFinite(rawDuration) && rawDuration > 0
|
|
67
|
+
? rawDuration
|
|
68
|
+
: FALLBACK_TIMELINE_FILE_DROP_DURATION;
|
|
76
69
|
const start = nextStart;
|
|
77
70
|
nextStart = roundToCenti(nextStart + duration);
|
|
78
|
-
return { start, track };
|
|
71
|
+
return { start, track: placement.track };
|
|
79
72
|
});
|
|
80
73
|
}
|
|
81
74
|
|
|
82
|
-
export function
|
|
83
|
-
left: number;
|
|
84
|
-
top: number;
|
|
75
|
+
export function resolveTimelineAssetCompositionSize(source: string): {
|
|
85
76
|
width: number;
|
|
86
77
|
height: number;
|
|
87
78
|
} {
|
|
88
79
|
const width = Number.parseFloat(source.match(/\bdata-width=(["'])([^"']+)\1/i)?.[2] ?? "");
|
|
89
80
|
const height = Number.parseFloat(source.match(/\bdata-height=(["'])([^"']+)\1/i)?.[2] ?? "");
|
|
90
|
-
|
|
91
81
|
return {
|
|
92
|
-
left: 0,
|
|
93
|
-
top: 0,
|
|
94
82
|
width: Number.isFinite(width) && width > 0 ? Math.round(width) : 640,
|
|
95
83
|
height: Number.isFinite(height) && height > 0 ? Math.round(height) : 360,
|
|
96
84
|
};
|
|
97
85
|
}
|
|
98
86
|
|
|
87
|
+
/**
|
|
88
|
+
* CapCut-style placement: natural size when it fits, scaled-to-fit when
|
|
89
|
+
* oversized, always centered. Unknown natural size → full-frame.
|
|
90
|
+
*/
|
|
91
|
+
export function fitTimelineAssetGeometry(
|
|
92
|
+
natural: { width: number; height: number } | null,
|
|
93
|
+
comp: { width: number; height: number },
|
|
94
|
+
): { left: number; top: number; width: number; height: number } {
|
|
95
|
+
if (!natural || natural.width <= 0 || natural.height <= 0) {
|
|
96
|
+
return { left: 0, top: 0, width: comp.width, height: comp.height };
|
|
97
|
+
}
|
|
98
|
+
const scale = Math.min(1, comp.width / natural.width, comp.height / natural.height);
|
|
99
|
+
const width = Math.round(natural.width * scale);
|
|
100
|
+
const height = Math.round(natural.height * scale);
|
|
101
|
+
return {
|
|
102
|
+
left: Math.round((comp.width - width) / 2),
|
|
103
|
+
top: Math.round((comp.height - height) / 2),
|
|
104
|
+
width,
|
|
105
|
+
height,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
export function buildTimelineAssetInsertHtml(input: {
|
|
100
110
|
id: string;
|
|
111
|
+
hfId: string;
|
|
101
112
|
assetPath: string;
|
|
102
113
|
kind: TimelineAssetKind;
|
|
103
114
|
start: number;
|
|
@@ -106,7 +117,7 @@ export function buildTimelineAssetInsertHtml(input: {
|
|
|
106
117
|
zIndex: number;
|
|
107
118
|
geometry?: { left: number; top: number; width: number; height: number };
|
|
108
119
|
}): string {
|
|
109
|
-
const sharedAttrs = `id="${input.id}" class="clip" src="${input.assetPath}" data-start="${input.start}" data-duration="${input.duration}" data-track-index="${input.track}"`;
|
|
120
|
+
const sharedAttrs = `id="${input.id}" data-hf-id="${input.hfId}" class="clip" src="${input.assetPath}" data-start="${input.start}" data-duration="${input.duration}" data-track-index="${input.track}"`;
|
|
110
121
|
const geometry = input.geometry ?? { left: 0, top: 0, width: 640, height: 360 };
|
|
111
122
|
const visualStyles = `position: absolute; left: ${geometry.left}px; top: ${geometry.top}px; width: ${geometry.width}px; height: ${geometry.height}px; object-fit: contain; z-index: ${input.zIndex}`;
|
|
112
123
|
|
|
@@ -118,7 +129,34 @@ export function buildTimelineAssetInsertHtml(input: {
|
|
|
118
129
|
return `<video ${sharedAttrs} muted playsinline style="${visualStyles}"></video>`;
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
return `<audio ${sharedAttrs} style="z-index: ${input.zIndex}"></audio>`;
|
|
132
|
+
return `<audio ${sharedAttrs} data-volume="1" style="z-index: ${input.zIndex}"></audio>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A clip inserted past the composition end would exist in the HTML but never
|
|
137
|
+
* appear on the timeline or in playback. Extend the root's data-duration to
|
|
138
|
+
* cover it (mirrors blockInstaller's behavior for installed blocks).
|
|
139
|
+
*/
|
|
140
|
+
export function extendCompositionDurationIfNeeded(source: string, requiredEnd: number): string {
|
|
141
|
+
const rootDur = readRootCompositionDuration(source);
|
|
142
|
+
if (rootDur == null || !Number.isFinite(rootDur) || requiredEnd <= rootDur) return source;
|
|
143
|
+
return patchRootCompositionDuration(source, String(roundToCenti(requiredEnd)));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Set the composition root's `data-duration` to `contentEnd` (grow OR shrink) so the
|
|
148
|
+
* timeline length tracks content — the content-driven counterpart to
|
|
149
|
+
* extendCompositionDurationIfNeeded's grow-only ratchet. Used after edits that can
|
|
150
|
+
* reduce the furthest clip end (delete/trim). No-op when `contentEnd` is not > 0, so
|
|
151
|
+
* an empty timeline keeps its declared duration instead of collapsing to 0.
|
|
152
|
+
*/
|
|
153
|
+
export function setCompositionDurationToContent(source: string, contentEnd: number): string {
|
|
154
|
+
if (!Number.isFinite(contentEnd) || contentEnd <= 0) return source;
|
|
155
|
+
const rootDur = readRootCompositionDuration(source);
|
|
156
|
+
if (rootDur == null) return source;
|
|
157
|
+
const next = roundToCenti(contentEnd);
|
|
158
|
+
if (rootDur === next) return source;
|
|
159
|
+
return patchRootCompositionDuration(source, String(next));
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
export function insertTimelineAssetIntoSource(source: string, assetHtml: string): string {
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
export const TIMELINE_TOGGLE_SHORTCUT_LABEL = "Shift+T";
|
|
2
|
-
type TimelineToggleHotkeyEvent = Pick<
|
|
3
|
-
KeyboardEvent,
|
|
4
|
-
"key" | "shiftKey" | "metaKey" | "ctrlKey" | "altKey" | "target"
|
|
5
|
-
>;
|
|
6
|
-
|
|
7
1
|
interface EditableTargetLike {
|
|
8
2
|
tagName?: string;
|
|
9
3
|
isContentEditable?: boolean;
|
|
@@ -28,14 +22,3 @@ export function isEditableTarget(target: EventTarget | null): boolean {
|
|
|
28
22
|
),
|
|
29
23
|
);
|
|
30
24
|
}
|
|
31
|
-
|
|
32
|
-
export function shouldHandleTimelineToggleHotkey(event: TimelineToggleHotkeyEvent): boolean {
|
|
33
|
-
if (event.metaKey || event.ctrlKey || event.altKey) return false;
|
|
34
|
-
if (!event.shiftKey) return false;
|
|
35
|
-
if (event.key.toLowerCase() !== "t") return false;
|
|
36
|
-
return !isEditableTarget(event.target);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function getTimelineToggleTitle(timelineVisible: boolean): string {
|
|
40
|
-
return `${timelineVisible ? "Hide" : "Show"} timeline editor (${TIMELINE_TOGGLE_SHORTCUT_LABEL})`;
|
|
41
|
-
}
|