@hyperframes/studio 0.8.55 → 0.8.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-c23wnIfc.js +464 -0
- package/dist/assets/{index-DrHQAuGO.css → index-DKJpHYYd.css} +1 -1
- package/dist/assets/{index-BEIRytvV.js → index-DgkAzx3n.js} +1 -1
- package/dist/assets/{index-DaNpNFJC.js → index-IDNr3X1H.js} +1 -1
- package/dist/assets/{index-TcBZyzNA.js → index-gbOCDCuZ.js} +202 -202
- package/dist/index.d.ts +111 -17
- package/dist/index.html +2 -2
- package/dist/index.js +21534 -20683
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/App.tsx +18 -1
- package/src/components/EditorShell.readOnlyHostMount.test.tsx +116 -0
- package/src/components/EditorShell.selectionSync.test.tsx +57 -67
- package/src/components/EditorShell.tsx +62 -51
- package/src/components/EditorShellTestMocks.ts +46 -0
- package/src/components/ProjectUnreachableBanner.test.tsx +113 -0
- package/src/components/ProjectUnreachableBanner.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +19 -55
- package/src/components/editor/DomEditOverlayHover.test.tsx +1 -15
- package/src/components/editor/DomEditSelectionChrome.tsx +11 -9
- package/src/components/editor/GridOverlay.tsx +12 -23
- package/src/components/editor/MotionPathOverlay.test.tsx +66 -0
- package/src/components/editor/MotionPathOverlay.tsx +3 -1
- package/src/components/editor/PreviewGuides.test.tsx +6 -1
- package/src/components/editor/PreviewGuides.tsx +8 -10
- package/src/components/editor/PreviewOverlayProvider.test.tsx +90 -0
- package/src/components/editor/PreviewOverlayProvider.tsx +86 -0
- package/src/components/editor/PropertyPanel.deselect.test.tsx +71 -0
- package/src/components/editor/PropertyPanel.tsx +4 -3
- package/src/components/editor/SnapToolbar.test.tsx +26 -12
- package/src/components/editor/SnapToolbar.tsx +30 -59
- package/src/components/editor/domEditOverlayTestMocks.ts +16 -0
- package/src/components/editor/previewReadOnly.overlay.test.tsx +260 -0
- package/src/components/editor/previewReadOnlyContext.tsx +59 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +9 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +46 -12
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -1
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +3 -13
- package/src/components/editor/useDomEditNudge.ts +9 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +4 -0
- package/src/components/editor/useOffCanvasIndicators.ts +50 -0
- package/src/components/editor/usePreviewCompositionRect.test.tsx +19 -13
- package/src/components/editor/usePreviewCompositionRect.ts +12 -12
- package/src/components/editor/useVolumeAutomation.test.tsx +68 -0
- package/src/components/editor/useVolumeAutomation.ts +57 -16
- package/src/components/nle/NLEContext.tsx +40 -4
- package/src/components/nle/NLEPreview.test.ts +66 -1
- package/src/components/nle/NLEPreview.tsx +88 -44
- package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +4 -35
- package/src/components/nle/PreviewOverlays.tsx +19 -24
- package/src/components/nle/PreviewOverlaysTestMocks.ts +40 -0
- package/src/components/nle/PreviewPane.tsx +17 -2
- package/src/components/nle/TimelinePane.tsx +5 -1
- package/src/components/nle/previewReadOnly.test.tsx +129 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -6
- package/src/components/nle/usePreviewBlockDrop.ts +3 -1
- package/src/hooks/appHotkeysDispatch.ts +16 -1
- package/src/hooks/newTweenTargetHooks.test.tsx +1 -0
- package/src/hooks/timelineAudioGroupCreate.ts +3 -4
- package/src/hooks/timelineTrackVisibility.ts +7 -8
- package/src/hooks/useAppHotkeys.test.ts +71 -3
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
- package/src/hooks/useAppHotkeys.ts +4 -0
- package/src/hooks/useCompositionDimensions.test.tsx +38 -0
- package/src/hooks/useCompositionDimensions.ts +17 -1
- package/src/hooks/useDomEditAttributeCommits.test.ts +23 -0
- package/src/hooks/useDomEditAttributeCommits.ts +14 -3
- package/src/hooks/useDomEditCommits.test.tsx +1 -0
- package/src/hooks/useDomEditCommits.ts +3 -0
- package/src/hooks/useDomEditSession.test.tsx +1 -0
- package/src/hooks/useDomEditSession.ts +7 -4
- package/src/hooks/useDomEditWiring.ts +40 -40
- package/src/hooks/useDomGeometryCommits.test.tsx +85 -2
- package/src/hooks/useDomGeometryCommits.ts +26 -20
- package/src/hooks/useFileTree.test.tsx +56 -0
- package/src/hooks/useFileTree.ts +8 -1
- package/src/hooks/useGestureCommit.test.tsx +29 -0
- package/src/hooks/useGestureCommit.ts +23 -0
- package/src/hooks/useInlineTextEdit.test.tsx +23 -2
- package/src/hooks/useInlineTextEdit.ts +11 -2
- package/src/hooks/useSdkSession.lifecycle.test.tsx +80 -0
- package/src/hooks/useSdkSession.ts +46 -10
- package/src/hooks/useServerConnection.hashProject.test.tsx +303 -0
- package/src/hooks/useServerConnection.ts +81 -8
- package/src/hooks/useTimelineAssetDropOps.test.tsx +3 -4
- package/src/hooks/useTimelineAssetDropOps.ts +1 -6
- package/src/hooks/useTimelineEditing.test.tsx +10 -7
- package/src/hooks/useTimelineEditing.ts +2 -7
- package/src/index.ts +12 -0
- package/src/player/components/Player.test.ts +141 -13
- package/src/player/components/Player.tsx +58 -2
- package/src/player/components/ShortcutsPanel.test.tsx +4 -10
- package/src/player/components/Timeline.tsx +23 -573
- package/src/player/components/TimelineCanvas.tsx +9 -30
- package/src/player/components/TimelineClip.test.tsx +52 -0
- package/src/player/components/TimelineClip.tsx +13 -2
- package/src/player/components/TimelineOverlays.test.ts +67 -32
- package/src/player/components/TimelineOverlays.tsx +46 -107
- package/src/player/components/TimelineProvider.tsx +191 -0
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/testRootHarness.ts +18 -0
- package/src/player/components/timelineMotionStyles.test.ts +3 -3
- package/src/player/components/timelineProviderStateBuilders.test.ts +109 -0
- package/src/player/components/timelineProviderStateBuilders.ts +83 -0
- package/src/player/components/timelineTheme.test.ts +4 -4
- package/src/player/components/timelineTheme.ts +34 -30
- package/src/player/components/timelineTrackDisplay.ts +1 -4
- package/src/player/components/useTimelineClipContextMenu.ts +1 -1
- package/src/player/components/useTimelineOverlaysState.ts +100 -0
- package/src/player/components/useTimelineProviderState.tsx +583 -0
- package/src/player/components/useTimelineTrackDerivations.ts +2 -2
- package/src/player/hooks/playbackAdapterResolution.ts +98 -0
- package/src/player/hooks/timelinePlayerTestHarness.ts +120 -0
- package/src/player/hooks/timelineSyncHydration.test.ts +0 -18
- package/src/player/hooks/timelineSyncHydration.ts +0 -23
- package/src/player/hooks/usePreviewFirstFrameTelemetry.ts +51 -0
- package/src/player/hooks/useShadowPreviewReload.ts +183 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +8 -88
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +457 -0
- package/src/player/hooks/useTimelinePlayer.ts +65 -99
- package/src/player/hooks/useTimelineSyncCallbacks.ts +159 -91
- package/src/player/store/playerStore.ts +0 -4
- package/src/player/store/timelineElement.ts +2 -4
- package/src/player/store/timelineResetState.ts +0 -1
- package/src/styles/hex-baseline.json +3 -4
- package/src/styles/studio.css +7 -7
- package/src/styles/theme.css +29 -0
- package/src/telemetry/events.test.ts +18 -0
- package/src/telemetry/events.ts +10 -0
- package/src/telemetry/system.test.ts +49 -0
- package/src/telemetry/system.ts +6 -0
- package/src/telemetry/tabId.test.ts +43 -0
- package/src/telemetry/tabId.ts +44 -0
- package/src/utils/previewCompositionSize.ts +26 -0
- package/src/utils/reloadDebug.ts +2 -4
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioTelemetry.test.ts +14 -0
- package/src/utils/studioTelemetry.ts +4 -0
- package/dist/assets/hyperframes-player-BjAGc2ID.js +0 -464
- package/src/player/hooks/useTimelineRowElements.test.ts +0 -28
- package/src/player/hooks/useTimelineRowElements.ts +0 -17
|
@@ -5,7 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
5
5
|
import type { GsapAnimation, PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
|
6
6
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { TimelinePropertyLanes } from "./TimelinePropertyLanes";
|
|
8
|
-
import { TimelineTrackHeader } from "./TimelineTrackHeader";
|
|
8
|
+
import { TimelineTrackHeader, gutterFill } from "./TimelineTrackHeader";
|
|
9
9
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
10
10
|
import { type TimelineElement } from "../store/playerStore";
|
|
11
11
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
@@ -794,10 +794,6 @@ describe("TimelineTrackHeader", () => {
|
|
|
794
794
|
});
|
|
795
795
|
expect(header()?.style.paddingLeft).toBe("14px");
|
|
796
796
|
expect(header()?.style.borderLeft).toContain("2px");
|
|
797
|
-
// And a lighter gutter, so the row reads as sitting INSIDE its group
|
|
798
|
-
// rather than beside it. Overlaid on the theme's own fill rather than a
|
|
799
|
-
// hard-coded colour, so it follows whatever the gutter is.
|
|
800
|
-
expect(header()?.style.background).toContain("linear-gradient");
|
|
801
797
|
act(() => view.root.unmount());
|
|
802
798
|
});
|
|
803
799
|
|
|
@@ -912,3 +908,28 @@ describe("TimelineTrackHeader", () => {
|
|
|
912
908
|
});
|
|
913
909
|
});
|
|
914
910
|
});
|
|
911
|
+
|
|
912
|
+
// A host themes the timeline by overriding --timeline-* on theme.css, so the
|
|
913
|
+
// rendered gutter must carry the CSS variable, not a baked-in colour.
|
|
914
|
+
describe("theming", () => {
|
|
915
|
+
it("reads the gutter background and border from timeline theme tokens", () => {
|
|
916
|
+
const view = renderHeader({});
|
|
917
|
+
const header = view.host.querySelector<HTMLElement>('[role="rowheader"]');
|
|
918
|
+
expect(header?.style.background).toBe(defaultTimelineTheme.gutterBackground);
|
|
919
|
+
expect(header?.style.background).toContain("var(--timeline-gutter-bg)");
|
|
920
|
+
expect(header?.style.borderRight).toContain(defaultTimelineTheme.gutterBorder);
|
|
921
|
+
act(() => view.root.unmount());
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// happy-dom's `background` shorthand garbles a `var()` layer next to
|
|
925
|
+
// `linear-gradient(...)` (verified), so this checks the same value through
|
|
926
|
+
// the pure function instead of the broken CSSOM roundtrip.
|
|
927
|
+
it("overlays the group-member tint on the themed gutter, not a hard-coded colour", () => {
|
|
928
|
+
const filled = gutterFill(defaultTimelineTheme.gutterBackground, true);
|
|
929
|
+
expect(filled).toContain("linear-gradient");
|
|
930
|
+
expect(filled.endsWith(defaultTimelineTheme.gutterBackground)).toBe(true);
|
|
931
|
+
expect(gutterFill(defaultTimelineTheme.gutterBackground, false)).toBe(
|
|
932
|
+
defaultTimelineTheme.gutterBackground,
|
|
933
|
+
);
|
|
934
|
+
});
|
|
935
|
+
});
|
|
@@ -34,7 +34,7 @@ const GROUP_MEMBER_INDENT = 14;
|
|
|
34
34
|
const GROUP_MEMBER_TINT = "rgba(255,255,255,0.035)";
|
|
35
35
|
|
|
36
36
|
/** The gutter fill for a row, tinted when it belongs to a group. */
|
|
37
|
-
function gutterFill(base: string, isGroupMember: boolean): string {
|
|
37
|
+
export function gutterFill(base: string, isGroupMember: boolean): string {
|
|
38
38
|
return isGroupMember
|
|
39
39
|
? `linear-gradient(${GROUP_MEMBER_TINT}, ${GROUP_MEMBER_TINT}), ${base}`
|
|
40
40
|
: base;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach } from "vitest";
|
|
4
|
+
|
|
5
|
+
export function createHappyDomRootHarness() {
|
|
6
|
+
const roots: Root[] = [];
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
for (const root of roots.splice(0)) act(() => root.unmount());
|
|
9
|
+
document.body.innerHTML = "";
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
mount(host: HTMLElement) {
|
|
13
|
+
const root = createRoot(host);
|
|
14
|
+
roots.push(root);
|
|
15
|
+
return root;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -75,11 +75,11 @@ describe("timeline motion styles", () => {
|
|
|
75
75
|
const bloomOverlayRule = expectRule(studioCss, ".timeline-clip::before");
|
|
76
76
|
const activeBloomOverlayRule = expectRule(studioCss, ".timeline-clip[data-active]::before");
|
|
77
77
|
|
|
78
|
-
expect(baseTimelineClipRule).toContain("background-color:
|
|
78
|
+
expect(baseTimelineClipRule).toContain("background-color: var(--clip-bg)");
|
|
79
79
|
expect(activeTimelineClipRule).not.toContain("background: linear-gradient");
|
|
80
|
-
expect(activeTimelineClipRule).toContain("border-color:
|
|
80
|
+
expect(activeTimelineClipRule).toContain("border-color: var(--clip-border-active)");
|
|
81
81
|
expect(activeTimelineClipRule).not.toContain("box-shadow");
|
|
82
|
-
expect(bloomOverlayRule).toContain("background:
|
|
82
|
+
expect(bloomOverlayRule).toContain("background: var(--clip-bg-active)");
|
|
83
83
|
expect(bloomOverlayRule).not.toContain("linear-gradient");
|
|
84
84
|
expect(bloomOverlayRule).toContain("opacity: 0");
|
|
85
85
|
expect(activeBloomOverlayRule).toContain("opacity: 1");
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
buildTimelineMeta,
|
|
6
|
+
resolveRenderClipContent,
|
|
7
|
+
resolveResizingElementIds,
|
|
8
|
+
shouldIgnoreTimelinePointerDown,
|
|
9
|
+
type TimelineMetaBuilderInputs,
|
|
10
|
+
} from "./timelineProviderStateBuilders";
|
|
11
|
+
|
|
12
|
+
function inputs(overrides: Partial<TimelineMetaBuilderInputs> = {}): TimelineMetaBuilderInputs {
|
|
13
|
+
return {
|
|
14
|
+
emptyState: {} as TimelineMetaBuilderInputs["emptyState"],
|
|
15
|
+
container: {
|
|
16
|
+
ref: vi.fn(),
|
|
17
|
+
"aria-label": "Timeline track view",
|
|
18
|
+
"data-timeline-element-count": 2,
|
|
19
|
+
isDragOver: false,
|
|
20
|
+
activeTool: "select",
|
|
21
|
+
shiftHeld: false,
|
|
22
|
+
accentClass: "ring",
|
|
23
|
+
onMouseMove: vi.fn(),
|
|
24
|
+
onMouseLeave: vi.fn(),
|
|
25
|
+
style: {},
|
|
26
|
+
},
|
|
27
|
+
viewport: {
|
|
28
|
+
ref: vi.fn(),
|
|
29
|
+
tabIndex: -1,
|
|
30
|
+
labelMode: false,
|
|
31
|
+
zoomMode: "fit",
|
|
32
|
+
onScroll: vi.fn(),
|
|
33
|
+
onFocus: vi.fn(),
|
|
34
|
+
onBlur: vi.fn(),
|
|
35
|
+
onDragOver: vi.fn(),
|
|
36
|
+
onDragLeave: vi.fn(),
|
|
37
|
+
onDrop: vi.fn(),
|
|
38
|
+
onPointerDown: vi.fn(),
|
|
39
|
+
onPointerMove: vi.fn(),
|
|
40
|
+
onPointerUp: vi.fn(),
|
|
41
|
+
onPointerCancel: vi.fn(),
|
|
42
|
+
onLostPointerCapture: vi.fn(),
|
|
43
|
+
},
|
|
44
|
+
elementCount: 2,
|
|
45
|
+
labelColumnWidth: 120,
|
|
46
|
+
razorGuide: null,
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("buildTimelineMeta", () => {
|
|
52
|
+
it("uses the razor branch for the container class", () => {
|
|
53
|
+
const meta = buildTimelineMeta(
|
|
54
|
+
inputs({
|
|
55
|
+
container: {
|
|
56
|
+
...inputs().container,
|
|
57
|
+
activeTool: "razor",
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
expect(meta.containerProps.className).toContain("cursor-crosshair");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("uses the shift branch for the container class", () => {
|
|
65
|
+
const meta = buildTimelineMeta(
|
|
66
|
+
inputs({ container: { ...inputs().container, shiftHeld: true } }),
|
|
67
|
+
);
|
|
68
|
+
expect(meta.containerProps.className).toContain("cursor-crosshair");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("uses the default container class without an accent", () => {
|
|
72
|
+
const meta = buildTimelineMeta(inputs());
|
|
73
|
+
expect(meta.containerProps.className).toContain("cursor-default");
|
|
74
|
+
expect(meta.containerProps.className).not.toContain("ring");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("uses the label-column inset only in label mode", () => {
|
|
78
|
+
const fit = buildTimelineMeta(inputs());
|
|
79
|
+
const labels = buildTimelineMeta(
|
|
80
|
+
inputs({ viewport: { ...inputs().viewport, labelMode: true } }),
|
|
81
|
+
);
|
|
82
|
+
expect(fit.viewportProps["data-timeline-auto-scroll-left-inset"]).toBe(0);
|
|
83
|
+
expect(labels.viewportProps["data-timeline-auto-scroll-left-inset"]).toBe(120);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("timeline provider branch helpers", () => {
|
|
88
|
+
it("resolves virtualized clip content", () => {
|
|
89
|
+
const render = () => null;
|
|
90
|
+
expect(resolveRenderClipContent(true, true, render)).toBeUndefined();
|
|
91
|
+
expect(resolveRenderClipContent(false, true, render)).toBe(render);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("resolves resizing element ids", () => {
|
|
95
|
+
expect(resolveResizingElementIds(null)).toBeUndefined();
|
|
96
|
+
expect(resolveResizingElementIds({ element: { id: "clip" } } as never)).toEqual(["clip"]);
|
|
97
|
+
expect(resolveResizingElementIds({ groupPreview: [{ key: "group::clip" }] } as never)).toEqual([
|
|
98
|
+
"group::clip",
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("ignores pointer downs on interactive descendants only", () => {
|
|
103
|
+
const button = document.createElement("button");
|
|
104
|
+
const div = document.createElement("div");
|
|
105
|
+
expect(shouldIgnoreTimelinePointerDown(button)).toBe(true);
|
|
106
|
+
expect(shouldIgnoreTimelinePointerDown(div)).toBe(false);
|
|
107
|
+
expect(shouldIgnoreTimelinePointerDown(null)).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TimelineMeta,
|
|
3
|
+
TimelineContainerProps,
|
|
4
|
+
TimelineViewportProps,
|
|
5
|
+
} from "./TimelineProvider";
|
|
6
|
+
import type { ResizingClipState } from "./useTimelineClipDrag";
|
|
7
|
+
import type { TimelineLaneBaseProps } from "./timelineLaneProps";
|
|
8
|
+
|
|
9
|
+
export function resolveResizingElementIds(
|
|
10
|
+
resizingClip: ResizingClipState | null,
|
|
11
|
+
): readonly string[] | undefined {
|
|
12
|
+
if (resizingClip?.groupPreview) return resizingClip.groupPreview.map((change) => change.key);
|
|
13
|
+
if (resizingClip) return [resizingClip.element.key ?? resizingClip.element.id];
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveRenderClipContent(
|
|
18
|
+
rowVirtualizationActive: boolean,
|
|
19
|
+
isScrolling: boolean,
|
|
20
|
+
renderClipContent: TimelineLaneBaseProps["renderClipContent"],
|
|
21
|
+
): TimelineLaneBaseProps["renderClipContent"] {
|
|
22
|
+
return rowVirtualizationActive && isScrolling ? undefined : renderClipContent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function shouldIgnoreTimelinePointerDown(target: EventTarget | null): boolean {
|
|
26
|
+
return target instanceof Element && target.closest("button, input, select, a") !== null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ContainerInputs = Omit<TimelineContainerProps, "className"> & {
|
|
30
|
+
isDragOver: boolean;
|
|
31
|
+
activeTool: string;
|
|
32
|
+
shiftHeld: boolean;
|
|
33
|
+
accentClass: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ViewportInputs = Omit<
|
|
37
|
+
TimelineViewportProps,
|
|
38
|
+
"className" | "data-timeline-scroll-viewport" | "data-timeline-auto-scroll-left-inset"
|
|
39
|
+
> & {
|
|
40
|
+
labelMode: boolean;
|
|
41
|
+
zoomMode: "fit" | "manual";
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface TimelineMetaBuilderInputs {
|
|
45
|
+
emptyState: TimelineMeta["emptyState"];
|
|
46
|
+
container: ContainerInputs;
|
|
47
|
+
viewport: ViewportInputs;
|
|
48
|
+
elementCount: number;
|
|
49
|
+
labelColumnWidth: number;
|
|
50
|
+
razorGuide: TimelineMeta["razorGuide"];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildTimelineMeta({
|
|
54
|
+
emptyState,
|
|
55
|
+
container,
|
|
56
|
+
viewport,
|
|
57
|
+
elementCount,
|
|
58
|
+
labelColumnWidth,
|
|
59
|
+
razorGuide,
|
|
60
|
+
}: TimelineMetaBuilderInputs): TimelineMeta {
|
|
61
|
+
const { labelMode, zoomMode, ...viewportProps } = viewport;
|
|
62
|
+
const containerClassName = `relative border-t select-none h-full overflow-hidden ${container.isDragOver ? container.accentClass : ""} ${container.activeTool === "razor" ? "cursor-crosshair" : container.shiftHeld ? "cursor-crosshair" : "cursor-default"}`;
|
|
63
|
+
const viewportClassName = `${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-hidden`;
|
|
64
|
+
return {
|
|
65
|
+
emptyState,
|
|
66
|
+
containerProps: {
|
|
67
|
+
ref: container.ref,
|
|
68
|
+
"aria-label": container["aria-label"],
|
|
69
|
+
"data-timeline-element-count": elementCount,
|
|
70
|
+
className: containerClassName,
|
|
71
|
+
onMouseMove: container.onMouseMove,
|
|
72
|
+
onMouseLeave: container.onMouseLeave,
|
|
73
|
+
style: container.style,
|
|
74
|
+
},
|
|
75
|
+
viewportProps: {
|
|
76
|
+
...viewportProps,
|
|
77
|
+
"data-timeline-scroll-viewport": true,
|
|
78
|
+
"data-timeline-auto-scroll-left-inset": labelMode ? labelColumnWidth : 0,
|
|
79
|
+
className: viewportClassName,
|
|
80
|
+
},
|
|
81
|
+
razorGuide,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -9,10 +9,10 @@ import { getTrackStyle } from "./timelineIcons";
|
|
|
9
9
|
describe("getTimelineTrackStyle", () => {
|
|
10
10
|
it("uses one neutral clip style for every timeline tag", () => {
|
|
11
11
|
const expectedStyle = {
|
|
12
|
-
clip: "
|
|
13
|
-
clipActive: "
|
|
14
|
-
accent: "
|
|
15
|
-
label: "
|
|
12
|
+
clip: "var(--timeline-track-clip-fill)",
|
|
13
|
+
clipActive: "var(--timeline-track-clip-active)",
|
|
14
|
+
accent: "var(--color-accent)",
|
|
15
|
+
label: "var(--timeline-track-label)",
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
expect(getTimelineTrackStyle("video")).toEqual(expectedStyle);
|
|
@@ -22,6 +22,8 @@ export interface TimelineTheme {
|
|
|
22
22
|
tickMinor: string;
|
|
23
23
|
clipBackground: string;
|
|
24
24
|
clipBackgroundActive: string;
|
|
25
|
+
clipBackgroundHover: string;
|
|
26
|
+
clipBackgroundDragging: string;
|
|
25
27
|
clipBorder: string;
|
|
26
28
|
clipBorderHover: string;
|
|
27
29
|
clipBorderActive: string;
|
|
@@ -36,43 +38,45 @@ export interface TimelineTheme {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const TRACK_STYLE: TimelineTrackStyle = {
|
|
39
|
-
clip: "
|
|
40
|
-
clipActive: "
|
|
41
|
-
accent: "
|
|
42
|
-
label: "
|
|
41
|
+
clip: "var(--timeline-track-clip-fill)",
|
|
42
|
+
clipActive: "var(--timeline-track-clip-active)",
|
|
43
|
+
accent: "var(--color-accent)",
|
|
44
|
+
label: "var(--timeline-track-label)",
|
|
43
45
|
};
|
|
44
46
|
|
|
47
|
+
// Every field reads a CSS custom property (declared in styles/theme.css)
|
|
48
|
+
// rather than a literal, so a host themes the timeline the same way it
|
|
49
|
+
// themes the rest of Studio: by overriding the token, not this object.
|
|
45
50
|
export const defaultTimelineTheme: TimelineTheme = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
clipBorderActive: "rgba(255,255,255,0.24)",
|
|
51
|
+
shellBackground: "var(--timeline-shell-bg)",
|
|
52
|
+
shellBorder: "var(--timeline-shell-border)",
|
|
53
|
+
rulerBorder: "var(--timeline-ruler-border)",
|
|
54
|
+
rowBackground: "var(--timeline-row-bg)",
|
|
55
|
+
rowBorder: "var(--timeline-row-border)",
|
|
56
|
+
gutterBackground: "var(--timeline-gutter-bg)",
|
|
57
|
+
gutterBorder: "var(--timeline-gutter-border)",
|
|
58
|
+
textPrimary: "var(--timeline-text-primary)",
|
|
59
|
+
textSecondary: "var(--timeline-text-secondary)",
|
|
60
|
+
tickText: "var(--timeline-tick-text)",
|
|
61
|
+
tickMajor: "var(--timeline-tick-major)",
|
|
62
|
+
tickMinor: "var(--timeline-tick-minor)",
|
|
63
|
+
clipBackground: "var(--timeline-clip-bg)",
|
|
64
|
+
clipBackgroundActive: "var(--timeline-clip-bg-active)",
|
|
65
|
+
clipBackgroundHover: "var(--timeline-clip-bg-hover)",
|
|
66
|
+
clipBackgroundDragging: "var(--timeline-clip-bg-dragging)",
|
|
67
|
+
clipBorder: "var(--timeline-clip-border)",
|
|
68
|
+
clipBorderHover: "var(--timeline-clip-border-hover)",
|
|
69
|
+
clipBorderActive: "var(--timeline-clip-border-active)",
|
|
70
|
+
// Shadows stay literal: a light host still wants a dark contact shadow, so
|
|
71
|
+
// these are not part of the surface/hairline/text/clip-colour theming ask.
|
|
68
72
|
clipShadow: "none",
|
|
69
73
|
clipShadowHover: "0 2px 8px rgba(0,0,0,0.2)",
|
|
70
74
|
clipShadowActive: "0 2px 8px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.04)",
|
|
71
75
|
clipShadowDragging: "0 8px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06)",
|
|
72
|
-
handleColor: "
|
|
73
|
-
panelResizeSeam: "
|
|
74
|
-
panelResizeActive: "
|
|
75
|
-
clipRadius: "
|
|
76
|
+
handleColor: "var(--timeline-handle)",
|
|
77
|
+
panelResizeSeam: "var(--timeline-resize-seam)",
|
|
78
|
+
panelResizeActive: "var(--timeline-resize-active)",
|
|
79
|
+
clipRadius: "var(--radius-lg)",
|
|
76
80
|
};
|
|
77
81
|
|
|
78
82
|
export function getTimelineTrackStyle(_tag: string): TimelineTrackStyle {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one owner of "what track number does the user see".
|
|
3
3
|
*
|
|
4
|
-
* `TimelineElement.track` is a z-order SORT key, not a row number
|
|
5
|
-
* sub-composition child gets a synthesized fractional key (`host.track + n /
|
|
6
|
-
* (siblings + 2)`, see `useTimelineRowElements`), so putting it in a string
|
|
7
|
-
* announces "Hide track 0.16666666666666666". Every user-visible track number,
|
|
4
|
+
* `TimelineElement.track` is a z-order SORT key, not a row number. Every
|
|
8
5
|
* whether it is rendered by a component or baked into an undo-history label,
|
|
9
6
|
* routes through here; the raw key stays in callbacks and lookups only.
|
|
10
7
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
3
|
-
import type { ClipContextMenuState } from "./
|
|
3
|
+
import type { ClipContextMenuState } from "./TimelineProvider";
|
|
4
4
|
|
|
5
5
|
/** A clicked member of an active multi-selection keeps the whole group
|
|
6
6
|
* selected, so the context menu it opens acts on all of it. */
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
2
|
+
import type { TimelineOverlaysState } from "./TimelineProvider";
|
|
3
|
+
import type { TimelineTimeRange } from "../store/rangeSelectionSlice";
|
|
4
|
+
import { useClipContextMenu } from "./useTimelineClipContextMenu";
|
|
5
|
+
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
6
|
+
import { usePublishRangeSelection } from "./usePublishRangeSelection";
|
|
7
|
+
import { useTimelineSelectionLifecycle } from "./useTimelineSelectionLifecycle";
|
|
8
|
+
import { useTrackGapMenu, type TrackGapHighlight } from "./useTrackGapMenu";
|
|
9
|
+
|
|
10
|
+
type OverlayStateInputs = Omit<
|
|
11
|
+
TimelineOverlaysState,
|
|
12
|
+
| "elements"
|
|
13
|
+
| "elementsRef"
|
|
14
|
+
| "gapContextMenu"
|
|
15
|
+
| "onDismissGapContextMenu"
|
|
16
|
+
| "onCloseTrackGap"
|
|
17
|
+
| "onCloseAllTrackGaps"
|
|
18
|
+
| "onHoverGapAction"
|
|
19
|
+
| "rangeSelection"
|
|
20
|
+
| "setRangeSelection"
|
|
21
|
+
> & {
|
|
22
|
+
elements: TimelineElement[];
|
|
23
|
+
elementsRef: { current: TimelineElement[] };
|
|
24
|
+
rangeSelection: Parameters<typeof useTimelineRangeSelection>[0] & {
|
|
25
|
+
onRangeSelect?: (range: TimelineTimeRange | null) => void;
|
|
26
|
+
};
|
|
27
|
+
gapMenu: Parameters<typeof useTrackGapMenu>[0];
|
|
28
|
+
onSelectElement: ((element: TimelineElement | null) => void) | undefined;
|
|
29
|
+
selectedElementId: string | null;
|
|
30
|
+
setRangeSelectionRef: { current: ((selection: null) => void) | null };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type TimelineOverlaysStateResult = {
|
|
34
|
+
overlays: TimelineOverlaysState;
|
|
35
|
+
shiftClickClipRef: ReturnType<typeof useTimelineRangeSelection>["shiftClickClipRef"];
|
|
36
|
+
marqueeRect: ReturnType<typeof useTimelineRangeSelection>["marqueeRect"];
|
|
37
|
+
isScrubbing: ReturnType<typeof useTimelineRangeSelection>["isScrubbing"];
|
|
38
|
+
handlePointerDown: ReturnType<typeof useTimelineRangeSelection>["handlePointerDown"];
|
|
39
|
+
handlePointerMove: ReturnType<typeof useTimelineRangeSelection>["handlePointerMove"];
|
|
40
|
+
handlePointerUp: ReturnType<typeof useTimelineRangeSelection>["handlePointerUp"];
|
|
41
|
+
handlePointerCancel: ReturnType<typeof useTimelineRangeSelection>["handlePointerCancel"];
|
|
42
|
+
gapHighlight: TrackGapHighlight | null;
|
|
43
|
+
openGapMenu: ReturnType<typeof useTrackGapMenu>["openGapMenu"];
|
|
44
|
+
onContextMenuClip: ReturnType<typeof useClipContextMenu>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function useTimelineOverlaysState({
|
|
48
|
+
gapMenu,
|
|
49
|
+
rangeSelection: rangeSelectionInputs,
|
|
50
|
+
onSelectElement,
|
|
51
|
+
selectedElementId,
|
|
52
|
+
setRangeSelectionRef,
|
|
53
|
+
...state
|
|
54
|
+
}: OverlayStateInputs): TimelineOverlaysStateResult {
|
|
55
|
+
const {
|
|
56
|
+
rangeSelection,
|
|
57
|
+
setRangeSelection,
|
|
58
|
+
shiftClickClipRef,
|
|
59
|
+
marqueeRect,
|
|
60
|
+
isScrubbing,
|
|
61
|
+
handlePointerDown,
|
|
62
|
+
handlePointerMove,
|
|
63
|
+
handlePointerUp,
|
|
64
|
+
handlePointerCancel,
|
|
65
|
+
} = useTimelineRangeSelection(rangeSelectionInputs);
|
|
66
|
+
usePublishRangeSelection(rangeSelection, rangeSelectionInputs.onRangeSelect);
|
|
67
|
+
setRangeSelectionRef.current = () => setRangeSelection(null); // oxlint-disable-line react/refs -- stable ref consumed by useTimelineClipDrag
|
|
68
|
+
useTimelineSelectionLifecycle(state.elements, selectedElementId, state.setShowPopover, () =>
|
|
69
|
+
setRangeSelection(null),
|
|
70
|
+
);
|
|
71
|
+
const gap = useTrackGapMenu(gapMenu);
|
|
72
|
+
const onContextMenuClip = useClipContextMenu(
|
|
73
|
+
onSelectElement,
|
|
74
|
+
gap.dismissGapMenu,
|
|
75
|
+
state.setClipContextMenu,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
overlays: {
|
|
80
|
+
...state,
|
|
81
|
+
rangeSelection,
|
|
82
|
+
setRangeSelection,
|
|
83
|
+
gapContextMenu: gap.gapMenuModel,
|
|
84
|
+
onDismissGapContextMenu: gap.dismissGapMenu,
|
|
85
|
+
onCloseTrackGap: gap.closeTrackGap,
|
|
86
|
+
onCloseAllTrackGaps: gap.closeAllTrackGaps,
|
|
87
|
+
onHoverGapAction: gap.setHoveredGapAction,
|
|
88
|
+
},
|
|
89
|
+
shiftClickClipRef,
|
|
90
|
+
marqueeRect,
|
|
91
|
+
isScrubbing,
|
|
92
|
+
handlePointerDown,
|
|
93
|
+
handlePointerMove,
|
|
94
|
+
handlePointerUp,
|
|
95
|
+
handlePointerCancel,
|
|
96
|
+
gapHighlight: gap.gapHighlight,
|
|
97
|
+
openGapMenu: gap.openGapMenu,
|
|
98
|
+
onContextMenuClip,
|
|
99
|
+
};
|
|
100
|
+
}
|