@hyperframes/studio 0.8.138 → 0.8.140
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-B30apksz.js → hyperframes-player-BLuyxnK7.js} +1 -1
- package/dist/assets/{index-DXVvWfQG.js → index-CWfuDCoA.js} +1 -1
- package/dist/assets/{index-DIJuIBwI.js → index-Dr3Bo9uH.js} +1 -1
- package/dist/assets/{index-BdjA0CLD.js → index-VAHDM_zL.js} +210 -210
- package/dist/{chunk-4S3AYCOG.js → chunk-H2DUOXTF.js} +3 -2
- package/dist/{chunk-TDWGXBUT.js → chunk-TFLU2MGI.js} +1 -6
- package/dist/chunk-TFLU2MGI.js.map +1 -0
- package/dist/{chunk-7UQWVN52.js → chunk-UYWON5AX.js} +2 -2
- package/dist/{domEditingLayers-IEVLO54Z.js → domEditingLayers-KBTPQKGA.js} +3 -3
- package/dist/index.d.ts +20 -3
- package/dist/index.html +1 -1
- package/dist/index.js +7327 -6265
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/package.json +8 -8
- package/src/App.tsx +9 -4
- package/src/components/TimelineToolbar.test.tsx +79 -1
- package/src/components/TimelineToolbar.tsx +14 -29
- package/src/components/editor/DomEditOverlay.tsx +11 -7
- package/src/components/editor/PropertyPanel.test.tsx +600 -748
- package/src/components/editor/PropertyPanel.tsx +1 -0
- package/src/components/editor/PropertyPanelFlat.tsx +1 -0
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +2 -0
- package/src/components/editor/domEditOverlayGeometry.ts +7 -7
- package/src/components/editor/domEditOverlayGestures.ts +18 -0
- package/src/components/editor/domEditOverlayStartGesture.blocked.test.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.test.ts +3 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +9 -1
- package/src/components/editor/dragUndoPaint.test.ts +2 -0
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +432 -15
- package/src/components/editor/groupDropKeepsSelection.test.ts +1 -0
- package/src/components/editor/manualEdits.ts +1 -1
- package/src/components/editor/manualEditsDom.ts +7 -7
- package/src/components/editor/manualEditsParsing.ts +14 -2
- package/src/components/editor/manualEditsTypes.ts +5 -0
- package/src/components/editor/manualOffsetDrag.test.ts +23 -0
- package/src/components/editor/manualOffsetDrag.ts +5 -0
- package/src/components/editor/previewGestureStarts.ts +284 -0
- package/src/components/editor/previewReadOnly.overlay.test.tsx +3 -2
- package/src/components/editor/propertyPanelFill.tsx +36 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +4 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +13 -7
- package/src/components/editor/propertyPanelImageFill.test.tsx +164 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +10 -7
- package/src/components/editor/rotateInlineSave.test.ts +1 -0
- package/src/components/editor/shiftDragAxisLock.test.tsx +1 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +67 -71
- package/src/components/nle/NLEContext.test.ts +26 -0
- package/src/components/nle/NLEContext.tsx +8 -3
- package/src/components/nle/NLEPreview.test.ts +44 -1
- package/src/components/nle/NLEPreview.tsx +7 -2
- package/src/hooks/draggedGsapPosition.ts +8 -0
- package/src/hooks/editMoment.ts +13 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +25 -4
- package/src/hooks/gsapDragCommit.ts +2 -1
- package/src/hooks/gsapDragPositionCommit.ts +19 -7
- package/src/hooks/gsapPositionDetection.ts +5 -3
- package/src/hooks/gsapResizeIntercept.ts +11 -4
- package/src/hooks/gsapRuntimeBridge.ts +31 -11
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +3 -22
- package/src/hooks/gsapValueAtPlayhead.test.ts +15 -0
- package/src/hooks/gsapValueAtPlayhead.ts +12 -5
- package/src/hooks/timelineEditingHelpers.test.ts +22 -0
- package/src/hooks/timelineEditingHelpers.ts +18 -2
- package/src/hooks/useDomSelection.test.ts +21 -2
- package/src/hooks/useEditHistoryActions.test.tsx +20 -0
- package/src/hooks/useEditHistoryActions.ts +2 -0
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +536 -10
- package/src/hooks/useExternalFileChangeCoordinator.ts +72 -45
- package/src/hooks/useGsapAwareEditing.test.tsx +38 -0
- package/src/hooks/useGsapAwareEditing.ts +148 -136
- package/src/hooks/useGsapAwareGroupMove.ts +126 -113
- package/src/hooks/useGsapScriptCommits.ts +5 -4
- package/src/hooks/useMusicBeatAnalysis.ts +2 -1
- package/src/hooks/usePreviewPersistence.history.test.tsx +7 -2
- package/src/hooks/usePreviewPersistence.ts +1 -4
- package/src/hooks/useSdkSession.lifecycle.test.tsx +20 -0
- package/src/hooks/useSdkSession.ts +4 -1
- package/src/hooks/useThumbnailStripSize.test.tsx +65 -0
- package/src/hooks/useThumbnailStripSize.ts +23 -1
- package/src/hooks/useTimelineDeleteOps.test.tsx +10 -0
- package/src/hooks/useTimelineDeleteOps.ts +9 -10
- package/src/index.ts +2 -0
- package/src/player/components/AudioWaveform.test.tsx +153 -1
- package/src/player/components/AudioWaveform.tsx +93 -37
- package/src/player/components/Timeline.scrollRest.test.tsx +22 -0
- package/src/player/components/Timeline.test.ts +3 -1
- package/src/player/components/TimelineCanvas.tsx +4 -0
- package/src/player/components/TimelineClip.tsx +20 -6
- package/src/player/components/TimelineClipFades.test.tsx +11 -0
- package/src/player/components/TimelineClipFades.tsx +4 -4
- package/src/player/components/TimelineClipJoins.tsx +23 -18
- package/src/player/components/TimelineGestureOverlay.tsx +30 -27
- package/src/player/components/TimelineGroupRow.tsx +1 -0
- package/src/player/components/TimelineLaneClip.tsx +127 -0
- package/src/player/components/TimelineLanes.test.tsx +51 -6
- package/src/player/components/TimelineLanes.tsx +166 -178
- package/src/player/components/TimelineRuler.tsx +2 -0
- package/src/player/components/TimelineTimeLayer.tsx +25 -0
- package/src/player/components/VideoThumbnail.test.tsx +204 -12
- package/src/player/components/VideoThumbnail.tsx +37 -9
- package/src/player/components/thumbnailUtils.test.ts +17 -3
- package/src/player/components/thumbnailUtils.ts +17 -1
- package/src/player/components/timelineLayout.test.ts +3 -3
- package/src/player/components/timelineMotion.test.ts +27 -0
- package/src/player/components/timelineMotion.ts +62 -0
- package/src/player/components/timelineZoomInput.test.ts +438 -0
- package/src/player/components/timelineZoomInput.ts +456 -0
- package/src/player/components/useTimelineGeometry.ts +0 -3
- package/src/player/components/useTimelinePlayhead.test.tsx +97 -30
- package/src/player/components/useTimelinePlayhead.ts +53 -59
- package/src/player/components/useTimelineProviderState.tsx +16 -21
- package/src/player/components/useTimelineZoom.ts +7 -12
- package/src/player/hooks/useShadowPreviewReload.ts +6 -6
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +14 -0
- package/src/player/hooks/useTimelinePlayer.ts +8 -5
- package/src/player/lib/thumbnailScheduler.test.ts +20 -1
- package/src/player/lib/thumbnailScheduler.ts +1 -1
- package/src/player/lib/thumbnailVideoDecoder.test.ts +230 -30
- package/src/player/lib/thumbnailVideoDecoder.ts +197 -54
- package/src/player/lib/timelineElementHelpers.ts +0 -11
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/player/previewReloading.test.ts +48 -0
- package/src/player/previewReloading.ts +48 -0
- package/src/utils/gsapUndoRestore.browser.test.ts +4 -17
- package/src/utils/rootDuration.ts +5 -4
- package/src/utils/studioPendingEdits.test.ts +27 -0
- package/src/utils/studioPendingEdits.ts +9 -0
- package/src/utils/timelineAssetDrop.test.ts +13 -4
- package/src/utils/timelineAssetDrop.ts +6 -2
- package/dist/chunk-TDWGXBUT.js.map +0 -1
- /package/dist/{chunk-4S3AYCOG.js.map → chunk-H2DUOXTF.js.map} +0 -0
- /package/dist/{chunk-7UQWVN52.js.map → chunk-UYWON5AX.js.map} +0 -0
- /package/dist/{domEditingLayers-IEVLO54Z.js.map → domEditingLayers-KBTPQKGA.js.map} +0 -0
package/dist/player.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.140",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/core": "0.8.140",
|
|
64
|
+
"@hyperframes/parsers": "0.8.140",
|
|
65
|
+
"@hyperframes/sdk": "0.8.140",
|
|
66
|
+
"@hyperframes/player": "0.8.140",
|
|
67
|
+
"@hyperframes/studio-server": "0.8.140",
|
|
68
|
+
"@hyperframes/shader-transitions": "0.8.140"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"vite": "^6.4.2",
|
|
83
83
|
"vitest": "^4.1.11",
|
|
84
84
|
"zustand": "^5.0.0",
|
|
85
|
-
"@hyperframes/producer": "0.8.
|
|
85
|
+
"@hyperframes/producer": "0.8.140"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore";
|
|
2
2
|
import { buildProjectApiPath } from "./utils/projectRouting";
|
|
3
|
-
import { useState, useCallback, useRef, useMemo } from "react";
|
|
3
|
+
import { useState, useCallback, useRef, useMemo, type SetStateAction } from "react";
|
|
4
4
|
import { useStableHandlers } from "./hooks/useStableHandlers";
|
|
5
5
|
import { useHistoryFlags, useToolbarSession } from "./hooks/useShellSlices";
|
|
6
6
|
import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents";
|
|
@@ -62,6 +62,7 @@ import { useStudioSessionStart } from "./hooks/useStudioSessionStart";
|
|
|
62
62
|
import { useTimelineAddAtPlayhead } from "./hooks/useTimelineAddAtPlayhead";
|
|
63
63
|
import { readStudioUrlStateFromWindow, resolveMasterCompositionPath } from "./utils/studioUrlState";
|
|
64
64
|
import { useActiveComposition } from "./hooks/useActiveComposition";
|
|
65
|
+
import { requestPreviewReload } from "./player/previewReloading";
|
|
65
66
|
const getTimelineSelectionSet = () => usePlayerStore.getState().selectedElementIds;
|
|
66
67
|
|
|
67
68
|
export interface StudioAppProps {
|
|
@@ -79,7 +80,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
79
80
|
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
|
|
80
81
|
const previewIframe = useOwnPreviewIframe();
|
|
81
82
|
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
82
|
-
const [refreshKey,
|
|
83
|
+
const [refreshKey, setRefreshKeyState] = useState(0);
|
|
83
84
|
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
|
|
84
85
|
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
|
|
85
86
|
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
@@ -104,7 +105,11 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
104
105
|
const handleDomZIndexReorderCommitRef = useRef<TimelineZIndexReorderCommit | null>(null);
|
|
105
106
|
const pendingTimelineEditPathRef = useRef(new Set<string>());
|
|
106
107
|
const isGestureRecordingRef = useRef(false);
|
|
107
|
-
const
|
|
108
|
+
const setRefreshKey = useCallback((next: SetStateAction<number>) => {
|
|
109
|
+
requestPreviewReload();
|
|
110
|
+
setRefreshKeyState(next);
|
|
111
|
+
}, []);
|
|
112
|
+
const reloadPreview = useCallback(() => setRefreshKey((k) => k + 1), [setRefreshKey]);
|
|
108
113
|
const fileManagerResult = useFileManager({
|
|
109
114
|
projectId,
|
|
110
115
|
showToast,
|
|
@@ -144,7 +149,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
|
|
|
144
149
|
recordEdit: editHistory.recordEdit,
|
|
145
150
|
previewIframeRef,
|
|
146
151
|
activeCompPathRef,
|
|
147
|
-
reloadPreview
|
|
152
|
+
reloadPreview,
|
|
148
153
|
});
|
|
149
154
|
const previewPersistence = useStableHandlers(previewPersistenceResult, projectId);
|
|
150
155
|
const externalFileChanges = useStudioExternalFileChanges({
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
7
7
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
8
8
|
import { makeSelection } from "../hooks/domSelectionTestHarness";
|
|
@@ -11,6 +11,7 @@ import { readStudioUiPreferences } from "../utils/studioUiPreferences";
|
|
|
11
11
|
import { dispatchPlainKey, type HotkeyCallbacks } from "../hooks/appHotkeysDispatch";
|
|
12
12
|
import { AudioMeterStrip } from "./nle/AudioMeterStrip";
|
|
13
13
|
import { TimelineToolbar } from "./TimelineToolbar";
|
|
14
|
+
import { registerTimelineZoomViewport } from "../player/components/timelineZoomInput";
|
|
14
15
|
|
|
15
16
|
vi.mock("../contexts/StudioContext", () => ({
|
|
16
17
|
useStudioShellContextOptional: () => ({
|
|
@@ -239,6 +240,81 @@ describe("TimelineToolbar snap key", () => {
|
|
|
239
240
|
});
|
|
240
241
|
|
|
241
242
|
describe("TimelineToolbar Fit", () => {
|
|
243
|
+
// A zoom lands on the next frame; these tests step frames instead of waiting for them.
|
|
244
|
+
beforeEach(() => {
|
|
245
|
+
vi.useFakeTimers({
|
|
246
|
+
toFake: [
|
|
247
|
+
"requestAnimationFrame",
|
|
248
|
+
"cancelAnimationFrame",
|
|
249
|
+
"setTimeout",
|
|
250
|
+
"clearTimeout",
|
|
251
|
+
"performance",
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
afterEach(() => {
|
|
256
|
+
vi.useRealTimers();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
/** A mounted timeline for the zoom to preview on: 1080px wide, 32px of headers. */
|
|
260
|
+
function mountTimelineViewport() {
|
|
261
|
+
const scroll = document.createElement("div");
|
|
262
|
+
Object.defineProperties(scroll, {
|
|
263
|
+
clientWidth: { value: 1080 },
|
|
264
|
+
// At Fit the content is as wide as the view.
|
|
265
|
+
scrollWidth: { value: 1080 },
|
|
266
|
+
scrollLeft: { value: 0, writable: true },
|
|
267
|
+
});
|
|
268
|
+
usePlayerStore.setState({
|
|
269
|
+
zoomMode: "fit",
|
|
270
|
+
manualZoomPercent: 100,
|
|
271
|
+
timelineFitPps: 10,
|
|
272
|
+
timelinePps: 10,
|
|
273
|
+
});
|
|
274
|
+
return registerTimelineZoomViewport({ scroll, contentOrigin: 32 });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
it("moves the slider and its readout with a zoom while it is previewed", () => {
|
|
278
|
+
const unregister = mountTimelineViewport();
|
|
279
|
+
const { host, root } = renderToolbar();
|
|
280
|
+
const slider = host.querySelector<HTMLInputElement>('input[aria-label="Timeline zoom"]')!;
|
|
281
|
+
const before = slider.value;
|
|
282
|
+
act(() => {
|
|
283
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(
|
|
284
|
+
slider,
|
|
285
|
+
String(Number(before) + 5),
|
|
286
|
+
);
|
|
287
|
+
slider.dispatchEvent(new Event("input", { bubbles: true }));
|
|
288
|
+
});
|
|
289
|
+
act(() => vi.advanceTimersToNextFrame());
|
|
290
|
+
// Not laid out yet: the store still holds Fit, but the toolbar shows the zoom on screen.
|
|
291
|
+
expect(usePlayerStore.getState().zoomMode).toBe("fit");
|
|
292
|
+
expect(slider.value).not.toBe(before);
|
|
293
|
+
expect(host.querySelector('[aria-label="Timeline zoom level"]')?.textContent).not.toBe("Fit");
|
|
294
|
+
act(() => root.unmount());
|
|
295
|
+
act(() => unregister());
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("keeps Fit when it is picked while a zoom waits to be laid out", () => {
|
|
299
|
+
const unregister = mountTimelineViewport();
|
|
300
|
+
const { host, root } = renderToolbar();
|
|
301
|
+
act(() =>
|
|
302
|
+
host
|
|
303
|
+
.querySelector('button[aria-label="Zoom in"]')
|
|
304
|
+
?.dispatchEvent(new MouseEvent("click", { bubbles: true })),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
act(() =>
|
|
308
|
+
host
|
|
309
|
+
.querySelector('button[aria-label="Fit timeline to width"]')
|
|
310
|
+
?.dispatchEvent(new MouseEvent("click", { bubbles: true })),
|
|
311
|
+
);
|
|
312
|
+
act(() => vi.advanceTimersByTime(1000));
|
|
313
|
+
expect(usePlayerStore.getState().zoomMode).toBe("fit");
|
|
314
|
+
act(() => root.unmount());
|
|
315
|
+
act(() => unregister());
|
|
316
|
+
});
|
|
317
|
+
|
|
242
318
|
it("shows Fit as a named icon and says whether fit is on", () => {
|
|
243
319
|
const { host, root } = renderToolbar();
|
|
244
320
|
const fit = () => host.querySelector('button[aria-label="Fit timeline to width"]');
|
|
@@ -251,6 +327,7 @@ describe("TimelineToolbar Fit", () => {
|
|
|
251
327
|
.querySelector('button[aria-label="Zoom in"]')
|
|
252
328
|
?.dispatchEvent(new MouseEvent("click", { bubbles: true })),
|
|
253
329
|
);
|
|
330
|
+
act(() => vi.advanceTimersToNextFrame());
|
|
254
331
|
expect(fit()?.getAttribute("aria-pressed")).toBe("false");
|
|
255
332
|
act(() => root.unmount());
|
|
256
333
|
});
|
|
@@ -263,6 +340,7 @@ describe("TimelineToolbar Fit", () => {
|
|
|
263
340
|
.querySelector('button[aria-label="Zoom in"]')
|
|
264
341
|
?.dispatchEvent(new MouseEvent("click", { bubbles: true })),
|
|
265
342
|
);
|
|
343
|
+
act(() => vi.advanceTimersToNextFrame());
|
|
266
344
|
expect(usePlayerStore.getState().userZoomCount).toBe(before + 1);
|
|
267
345
|
act(() => root.unmount());
|
|
268
346
|
});
|
|
@@ -14,12 +14,14 @@ import {
|
|
|
14
14
|
import { computeElementPercentage, playsNear } from "../hooks/gsapShared";
|
|
15
15
|
import { useKeyframeKeyboard } from "../hooks/useKeyframeKeyboard";
|
|
16
16
|
import {
|
|
17
|
-
getNextTimelineZoomPercent,
|
|
18
|
-
getTimelineZoomPercent,
|
|
19
17
|
timelineZoomPercentToSlider,
|
|
20
18
|
timelineSliderToZoomPercent,
|
|
21
19
|
} from "../player/components/timelineZoom";
|
|
22
20
|
import { useTimelineZoom } from "../player/components/useTimelineZoom";
|
|
21
|
+
import {
|
|
22
|
+
useShownTimelineZoomPercent,
|
|
23
|
+
zoomTimelineStep,
|
|
24
|
+
} from "../player/components/timelineZoomInput";
|
|
23
25
|
import { usePlayerStore, type TimelineElement } from "../player";
|
|
24
26
|
import { Tooltip } from "./ui";
|
|
25
27
|
import { AudioMetersIcon } from "./icons/AudioMetersIcon";
|
|
@@ -170,17 +172,13 @@ export const TimelineToolbar = memo(function TimelineToolbar({
|
|
|
170
172
|
// Subscribe so the add-beat button reacts to playhead movement and analysis load.
|
|
171
173
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
172
174
|
const beatAnalysisReady = usePlayerStore((s) => s.beatAnalysis !== null);
|
|
173
|
-
//
|
|
174
|
-
// the selection changes, not only on the next playhead tick.
|
|
175
|
+
// Subscribed, so the split button follows the selection, not only playhead ticks.
|
|
175
176
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
176
177
|
const elements = usePlayerStore((s) => s.elements);
|
|
177
178
|
const timelineFitPps = usePlayerStore((s) => s.timelineFitPps);
|
|
178
|
-
const { zoomMode,
|
|
179
|
-
const displayedTimelineZoomPercent =
|
|
180
|
-
|
|
181
|
-
manualZoomPercent,
|
|
182
|
-
timelineFitPps,
|
|
183
|
-
);
|
|
179
|
+
const { zoomMode, setZoomMode, setManualZoomPercent } = useTimelineZoom();
|
|
180
|
+
const displayedTimelineZoomPercent = useShownTimelineZoomPercent();
|
|
181
|
+
const showsFit = zoomMode === "fit" && displayedTimelineZoomPercent === 100;
|
|
184
182
|
const {
|
|
185
183
|
state: keyframeState,
|
|
186
184
|
isMotionPath: keyframeIsMotionPath,
|
|
@@ -215,8 +213,6 @@ export const TimelineToolbar = memo(function TimelineToolbar({
|
|
|
215
213
|
}, []);
|
|
216
214
|
|
|
217
215
|
return (
|
|
218
|
-
// The "TIMELINE" label is dropped for CapCut-like density — the pane's
|
|
219
|
-
// position (tracks right below) makes it self-evident.
|
|
220
216
|
<div className="border-b border-neutral-800/60">
|
|
221
217
|
<div className="flex items-center justify-between px-2 py-0.5">
|
|
222
218
|
<div className="flex items-center gap-0.5">
|
|
@@ -510,12 +506,7 @@ export const TimelineToolbar = memo(function TimelineToolbar({
|
|
|
510
506
|
<button
|
|
511
507
|
type="button"
|
|
512
508
|
aria-label="Zoom out"
|
|
513
|
-
onClick={() =>
|
|
514
|
-
setZoomMode("manual");
|
|
515
|
-
setManualZoomPercent(
|
|
516
|
-
getNextTimelineZoomPercent("out", zoomMode, manualZoomPercent, timelineFitPps),
|
|
517
|
-
);
|
|
518
|
-
}}
|
|
509
|
+
onClick={() => zoomTimelineStep("out")}
|
|
519
510
|
className={flatIdle}
|
|
520
511
|
>
|
|
521
512
|
<MagnifyingGlassMinus size={16} aria-hidden="true" />
|
|
@@ -528,12 +519,11 @@ export const TimelineToolbar = memo(function TimelineToolbar({
|
|
|
528
519
|
value={timelineZoomPercentToSlider(displayedTimelineZoomPercent, timelineFitPps)}
|
|
529
520
|
title={`${displayedTimelineZoomPercent}%`}
|
|
530
521
|
aria-label="Timeline zoom"
|
|
531
|
-
onChange={(e) =>
|
|
532
|
-
setZoomMode("manual");
|
|
522
|
+
onChange={(e) =>
|
|
533
523
|
setManualZoomPercent(
|
|
534
524
|
timelineSliderToZoomPercent(Number(e.target.value), timelineFitPps),
|
|
535
|
-
)
|
|
536
|
-
}
|
|
525
|
+
)
|
|
526
|
+
}
|
|
537
527
|
// h-6 on the input is the 24x24 WCAG 2.2 (2.5.8) target: the visible
|
|
538
528
|
// track stays 2px and the thumb 10px, only the pointer box grows.
|
|
539
529
|
className="mx-1 h-6 w-[96px] cursor-pointer appearance-none bg-transparent [&::-webkit-slider-runnable-track]:h-[2px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-neutral-700 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-[10px] [&::-webkit-slider-thumb]:h-[10px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-text-0 [&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#0a0a0a,0_1px_3px_rgba(0,0,0,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb:active]:cursor-grabbing"
|
|
@@ -542,12 +532,7 @@ export const TimelineToolbar = memo(function TimelineToolbar({
|
|
|
542
532
|
<button
|
|
543
533
|
type="button"
|
|
544
534
|
aria-label="Zoom in"
|
|
545
|
-
onClick={() =>
|
|
546
|
-
setZoomMode("manual");
|
|
547
|
-
setManualZoomPercent(
|
|
548
|
-
getNextTimelineZoomPercent("in", zoomMode, manualZoomPercent, timelineFitPps),
|
|
549
|
-
);
|
|
550
|
-
}}
|
|
535
|
+
onClick={() => zoomTimelineStep("in")}
|
|
551
536
|
className={flatIdle}
|
|
552
537
|
>
|
|
553
538
|
<MagnifyingGlassPlus size={16} aria-hidden="true" />
|
|
@@ -558,7 +543,7 @@ export const TimelineToolbar = memo(function TimelineToolbar({
|
|
|
558
543
|
className="ml-1 w-[38px] text-right font-mono text-[11px] tabular-nums text-neutral-500 select-none"
|
|
559
544
|
aria-label="Timeline zoom level"
|
|
560
545
|
>
|
|
561
|
-
{
|
|
546
|
+
{showsFit ? "Fit" : `${displayedTimelineZoomPercent}%`}
|
|
562
547
|
</span>
|
|
563
548
|
</div>
|
|
564
549
|
</div>
|
|
@@ -10,6 +10,7 @@ import { useZOrderCrossedFlash, ZOrderCrossedFlash } from "./useZOrderCrossedFla
|
|
|
10
10
|
import { useCanvasContextMenuState } from "./useCanvasContextMenuState";
|
|
11
11
|
import {
|
|
12
12
|
type BlockedMoveState,
|
|
13
|
+
type WaitingPressState,
|
|
13
14
|
type DomEditGroupPathOffsetCommit,
|
|
14
15
|
type FocusableDomEditOverlay,
|
|
15
16
|
type MoveCommitOptions,
|
|
@@ -60,7 +61,7 @@ export interface DomEditOverlayProps {
|
|
|
60
61
|
hoverSelection: DomEditSelection | null;
|
|
61
62
|
allowCanvasMovement?: boolean;
|
|
62
63
|
allowBodyDrag?: boolean;
|
|
63
|
-
/** "host": no
|
|
64
|
+
/** "host": no marquee, box re-select, or body drag if allowBodyDrag is false; Enter still opens text. */
|
|
64
65
|
canvasInput?: "overlay" | "host";
|
|
65
66
|
onTextEditingChange?: (editing: boolean) => void;
|
|
66
67
|
/** A click on a single selection's box, in either mode; the event may be the pointerup. */
|
|
@@ -166,6 +167,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
166
167
|
const gestureRef = useRef<GestureState | null>(null);
|
|
167
168
|
const groupGestureRef = useRef<GroupGestureState | null>(null);
|
|
168
169
|
const blockedMoveRef = useRef<BlockedMoveState | null>(null);
|
|
170
|
+
const waitingPressRef = useRef<WaitingPressState | null>(null);
|
|
169
171
|
const suppressNextBoxClickRef = useRef(false);
|
|
170
172
|
const snapGuidesRef = useRef<SnapGuidesState | null>(null);
|
|
171
173
|
const rafPausedRef = useRef(false);
|
|
@@ -273,6 +275,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
273
275
|
});
|
|
274
276
|
|
|
275
277
|
const gestures = createDomEditOverlayGestureHandlers({
|
|
278
|
+
activeCompositionPathRef,
|
|
276
279
|
overlayRef,
|
|
277
280
|
iframeRef,
|
|
278
281
|
boxRef,
|
|
@@ -283,6 +286,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
283
286
|
gestureRef,
|
|
284
287
|
groupGestureRef,
|
|
285
288
|
blockedMoveRef,
|
|
289
|
+
waitingPressRef,
|
|
286
290
|
rafPausedRef,
|
|
287
291
|
suppressNextBoxClickRef,
|
|
288
292
|
setOverlayRect,
|
|
@@ -299,17 +303,17 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
299
303
|
});
|
|
300
304
|
|
|
301
305
|
useEffect(() => {
|
|
302
|
-
if (readOnly) gestures.clearPointerState(selectionRef);
|
|
306
|
+
if (readOnly) gestures.clearPointerState(selectionRef, true);
|
|
303
307
|
}, [gestures, readOnly, selectionRef]);
|
|
304
308
|
// A gesture that loses its pointer, the window or the overlay is cancelled, so its mark goes too.
|
|
305
|
-
const cancelGestureRef = useRef(() => {});
|
|
306
|
-
cancelGestureRef.current = () => gestures.clearPointerState(selectionRef);
|
|
309
|
+
const cancelGestureRef = useRef((_dropQueued?: boolean) => {});
|
|
310
|
+
cancelGestureRef.current = (dropQueued) => gestures.clearPointerState(selectionRef, dropQueued);
|
|
307
311
|
useMountEffect(() => {
|
|
308
312
|
const cancel = () => cancelGestureRef.current();
|
|
309
313
|
window.addEventListener("blur", cancel);
|
|
310
314
|
return () => {
|
|
311
315
|
window.removeEventListener("blur", cancel);
|
|
312
|
-
|
|
316
|
+
cancelGestureRef.current(true);
|
|
313
317
|
};
|
|
314
318
|
});
|
|
315
319
|
|
|
@@ -493,10 +497,10 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
493
497
|
onPointerLeave={() => onCanvasPointerLeaveRef.current()}
|
|
494
498
|
onPointerUp={marquee.onPointerUp}
|
|
495
499
|
onPointerCancel={marquee.onPointerCancel}
|
|
496
|
-
onLostPointerCapture={
|
|
500
|
+
onLostPointerCapture={gestures.onLostPointerCapture}
|
|
497
501
|
onContextMenu={hostInput ? undefined : handleContextMenu}
|
|
498
502
|
>
|
|
499
|
-
{
|
|
503
|
+
{hoverSelection && hoverRect && compRect.width > 0 && (
|
|
500
504
|
<div
|
|
501
505
|
aria-hidden="true"
|
|
502
506
|
data-dom-edit-hover-box="true"
|