@hyperframes/studio 0.8.54 → 0.8.56
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-Cy_AwUa8.js → hyperframes-player-k8LzMtXX.js} +1 -1
- package/dist/assets/{index-DPUJoO8n.js → index-BlkJ_Axe.js} +192 -192
- package/dist/assets/{index-CTa-6Kso.js → index-CxR7F2YQ.js} +1 -1
- package/dist/assets/{index-DkAzKuPZ.js → index-DFlILHJe.js} +1 -1
- package/dist/assets/{index-DrHQAuGO.css → index-ZmBtVrEm.css} +1 -1
- package/dist/index.d.ts +16 -8
- package/dist/index.html +2 -2
- package/dist/index.js +3152 -3010
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/App.tsx +14 -1
- package/src/components/EditorShell.readOnlyHostMount.test.tsx +116 -0
- package/src/components/EditorShell.selectionSync.test.tsx +57 -67
- package/src/components/EditorShell.tsx +59 -50
- package/src/components/EditorShellTestMocks.ts +46 -0
- package/src/components/editor/DomEditOverlay.tsx +19 -40
- package/src/components/editor/DomEditOverlayHover.test.tsx +1 -15
- package/src/components/editor/DomEditSelectionChrome.tsx +11 -9
- 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 +1 -0
- 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/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/nle/PreviewOverlays.topologyLens.test.tsx +4 -35
- package/src/components/nle/PreviewOverlays.tsx +15 -8
- package/src/components/nle/PreviewOverlaysTestMocks.ts +40 -0
- package/src/components/nle/TimelinePane.tsx +5 -1
- package/src/components/nle/previewReadOnly.test.tsx +128 -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/gsapDragCommit.ts +1 -1
- package/src/hooks/gsapEditOutcome.test.ts +115 -0
- package/src/hooks/gsapEditOutcome.ts +39 -10
- package/src/hooks/gsapResizeIntercept.test.ts +42 -3
- package/src/hooks/gsapResizeIntercept.ts +12 -2
- package/src/hooks/gsapRuntimeBridge.test.ts +10 -2
- package/src/hooks/gsapRuntimeBridge.ts +14 -3
- 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/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/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/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/player/components/Timeline.tsx +22 -23
- package/src/player/components/TimelineClip.test.tsx +52 -0
- package/src/player/components/TimelineClip.tsx +13 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
- package/src/player/components/TimelineTrackHeader.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +3 -3
- 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/useTimelineTrackDerivations.ts +2 -2
- package/src/player/hooks/timelineSyncHydration.test.ts +0 -18
- package/src/player/hooks/timelineSyncHydration.ts +0 -23
- package/src/player/hooks/useTimelineSyncCallbacks.ts +0 -3
- 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/utils/studioSaveDiagnostics.ts +12 -0
- package/src/player/hooks/useTimelineRowElements.test.ts +0 -28
- package/src/player/hooks/useTimelineRowElements.ts +0 -17
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useRef, useMemo, useCallback, useState, memo } from "react";
|
|
2
2
|
import { useAdjustedBeatAnalysis, useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis";
|
|
3
3
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
4
|
-
import { useTimelineRowElements } from "../hooks/useTimelineRowElements";
|
|
5
4
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
6
5
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
7
6
|
import { usePublishRangeSelection } from "./usePublishRangeSelection";
|
|
@@ -113,7 +112,7 @@ export const Timeline = memo(function Timeline({
|
|
|
113
112
|
const refreshAfterLaneMove = useTimelineLaneMoveRefresh();
|
|
114
113
|
useMusicBeatAnalysis();
|
|
115
114
|
const rawElements = usePlayerStore((s) => s.elements);
|
|
116
|
-
const
|
|
115
|
+
const timelineElements = rawElements;
|
|
117
116
|
const adjustedBeatAnalysis = useAdjustedBeatAnalysis();
|
|
118
117
|
const duration = usePlayerStore((s) => s.duration);
|
|
119
118
|
const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
|
|
@@ -123,8 +122,8 @@ export const Timeline = memo(function Timeline({
|
|
|
123
122
|
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
124
123
|
const gsapAnimations = usePlayerStore((s) => s.gsapAnimations);
|
|
125
124
|
const labelMode = useMemo(
|
|
126
|
-
() => timelineNeedsLabelColumn(gsapAnimations,
|
|
127
|
-
[gsapAnimations,
|
|
125
|
+
() => timelineNeedsLabelColumn(gsapAnimations, timelineElements),
|
|
126
|
+
[gsapAnimations, timelineElements],
|
|
128
127
|
);
|
|
129
128
|
// The label column provides pre-t=0 space; otherwise keep TRACKS_LEFT_PAD after the gutter.
|
|
130
129
|
const contentOrigin = labelMode ? LABEL_COL_W + GUTTER : GUTTER + TRACKS_LEFT_PAD;
|
|
@@ -163,13 +162,13 @@ export const Timeline = memo(function Timeline({
|
|
|
163
162
|
groups,
|
|
164
163
|
trackGroupOf,
|
|
165
164
|
} = useTimelineTrackLayout(
|
|
166
|
-
|
|
165
|
+
timelineElements,
|
|
167
166
|
gsapAnimations,
|
|
168
167
|
selectedElementId,
|
|
169
168
|
selectedElementIds,
|
|
170
169
|
);
|
|
171
|
-
const
|
|
172
|
-
|
|
170
|
+
const timelineElementsRef = useRef(timelineElements);
|
|
171
|
+
timelineElementsRef.current = timelineElements; // oxlint-disable-line react/refs -- event handlers read the latest elements
|
|
173
172
|
const ppsRef = useRef(100);
|
|
174
173
|
const durationRef = useRef(effectiveDuration);
|
|
175
174
|
durationRef.current = effectiveDuration; // oxlint-disable-line react/refs -- event handlers read the latest duration
|
|
@@ -198,7 +197,7 @@ export const Timeline = memo(function Timeline({
|
|
|
198
197
|
onCompositionDrop,
|
|
199
198
|
});
|
|
200
199
|
const { readClipZIndex, applyStackingPatches, zSyncEnabled } = useTimelineStackingSync({
|
|
201
|
-
expandedElementsRef,
|
|
200
|
+
expandedElementsRef: timelineElementsRef,
|
|
202
201
|
});
|
|
203
202
|
const {
|
|
204
203
|
gapMenuModel,
|
|
@@ -210,7 +209,7 @@ export const Timeline = memo(function Timeline({
|
|
|
210
209
|
closeAllTrackGaps,
|
|
211
210
|
} = useTrackGapMenu({
|
|
212
211
|
tracks,
|
|
213
|
-
expandedElementsRef,
|
|
212
|
+
expandedElementsRef: timelineElementsRef,
|
|
214
213
|
trackOrderRef,
|
|
215
214
|
onMoveElement: pinnedOnMoveElement,
|
|
216
215
|
onMoveElements: pinnedOnMoveElements,
|
|
@@ -259,14 +258,14 @@ export const Timeline = memo(function Timeline({
|
|
|
259
258
|
resizingClip?.groupPreview?.map((change) => change.key) ??
|
|
260
259
|
(resizingClip ? [getTimelineElementIdentity(resizingClip.element)] : undefined);
|
|
261
260
|
const { recordTimelineScroll } = useTimelinePerformanceTelemetry({
|
|
262
|
-
totalClipCount:
|
|
261
|
+
totalClipCount: timelineElements.length,
|
|
263
262
|
totalRowCount: displayLayout.displayTrackOrder.length,
|
|
264
263
|
zoomMode,
|
|
265
264
|
});
|
|
266
265
|
const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
|
|
267
266
|
useTimelineScrollViewport(scrollRef, [
|
|
268
267
|
timelineReady,
|
|
269
|
-
|
|
268
|
+
timelineElements.length,
|
|
270
269
|
displayLayout.totalH,
|
|
271
270
|
]);
|
|
272
271
|
const { pps, fitPps, displayContentWidth, displayDuration, zoomModeRef, manualZoomPercentRef } =
|
|
@@ -279,7 +278,7 @@ export const Timeline = memo(function Timeline({
|
|
|
279
278
|
fitPpsRef,
|
|
280
279
|
draggedClip,
|
|
281
280
|
resizingClip,
|
|
282
|
-
expandedElements,
|
|
281
|
+
expandedElements: timelineElements,
|
|
283
282
|
isDragging,
|
|
284
283
|
scrollRef,
|
|
285
284
|
lastScrollLeftRef,
|
|
@@ -295,7 +294,7 @@ export const Timeline = memo(function Timeline({
|
|
|
295
294
|
groups,
|
|
296
295
|
trackGroupOf,
|
|
297
296
|
gsapAnimations,
|
|
298
|
-
elements:
|
|
297
|
+
elements: timelineElements,
|
|
299
298
|
pixelsPerSecond: pps,
|
|
300
299
|
contentOrigin,
|
|
301
300
|
allowHorizontal: zoomMode === "manual",
|
|
@@ -312,7 +311,7 @@ export const Timeline = memo(function Timeline({
|
|
|
312
311
|
const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
|
|
313
312
|
const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
|
|
314
313
|
useTimelineKeyframeHandlers({
|
|
315
|
-
expandedElements,
|
|
314
|
+
expandedElements: timelineElements,
|
|
316
315
|
keyframeCache,
|
|
317
316
|
onSelectElement,
|
|
318
317
|
onSeek,
|
|
@@ -343,14 +342,14 @@ export const Timeline = memo(function Timeline({
|
|
|
343
342
|
scrollRef,
|
|
344
343
|
currentTime,
|
|
345
344
|
clipStateVersion: renderTimeRange,
|
|
346
|
-
elementStateVersion:
|
|
345
|
+
elementStateVersion: timelineElements,
|
|
347
346
|
});
|
|
348
347
|
const laneGapStrips = useTimelineGapHighlights({
|
|
349
348
|
gapHighlight,
|
|
350
349
|
tracks,
|
|
351
350
|
selectedElementId,
|
|
352
351
|
selectedElementIds,
|
|
353
|
-
expandedElements,
|
|
352
|
+
expandedElements: timelineElements,
|
|
354
353
|
dragActive: draggedClip?.started === true || resizingClip != null,
|
|
355
354
|
displayDuration,
|
|
356
355
|
});
|
|
@@ -371,7 +370,7 @@ export const Timeline = memo(function Timeline({
|
|
|
371
370
|
effectiveDuration,
|
|
372
371
|
pps,
|
|
373
372
|
timelineReady,
|
|
374
|
-
elementsLength:
|
|
373
|
+
elementsLength: timelineElements.length,
|
|
375
374
|
setZoomMode,
|
|
376
375
|
setManualZoomPercent,
|
|
377
376
|
onSeek,
|
|
@@ -407,7 +406,7 @@ export const Timeline = memo(function Timeline({
|
|
|
407
406
|
dragScrollRaf,
|
|
408
407
|
isDragging,
|
|
409
408
|
setShowPopover,
|
|
410
|
-
elementsRef:
|
|
409
|
+
elementsRef: timelineElementsRef,
|
|
411
410
|
clipIndex,
|
|
412
411
|
rowGeometryRef,
|
|
413
412
|
onSelectElement,
|
|
@@ -417,7 +416,7 @@ export const Timeline = memo(function Timeline({
|
|
|
417
416
|
usePublishRangeSelection(rangeSelection, onRangeSelect);
|
|
418
417
|
setRangeSelectionRef.current = setRangeSelection; // oxlint-disable-line react/refs -- stable ref consumed by useTimelineClipDrag
|
|
419
418
|
|
|
420
|
-
useTimelineSelectionLifecycle(
|
|
419
|
+
useTimelineSelectionLifecycle(timelineElements, selectedElementId, setShowPopover, () =>
|
|
421
420
|
setRangeSelection(null),
|
|
422
421
|
);
|
|
423
422
|
|
|
@@ -433,7 +432,7 @@ export const Timeline = memo(function Timeline({
|
|
|
433
432
|
[resizingClip],
|
|
434
433
|
);
|
|
435
434
|
|
|
436
|
-
if (!timelineReady ||
|
|
435
|
+
if (!timelineReady || timelineElements.length === 0) {
|
|
437
436
|
return (
|
|
438
437
|
<TimelineEmptyState
|
|
439
438
|
isDragOver={assetDrop.isDragOver}
|
|
@@ -449,7 +448,7 @@ export const Timeline = memo(function Timeline({
|
|
|
449
448
|
<div
|
|
450
449
|
ref={setContainerRef}
|
|
451
450
|
aria-label="Timeline track view"
|
|
452
|
-
data-timeline-element-count={
|
|
451
|
+
data-timeline-element-count={timelineElements.length}
|
|
453
452
|
className={`relative border-t select-none h-full overflow-hidden ${assetDrop.isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : ""} ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
|
|
454
453
|
onMouseMove={updateRazorGuide}
|
|
455
454
|
onMouseLeave={clearRazorGuide}
|
|
@@ -566,8 +565,8 @@ export const Timeline = memo(function Timeline({
|
|
|
566
565
|
{activeTool === "razor" && razorGuideX !== null && <TimelineRazorGuide x={razorGuideX} />}
|
|
567
566
|
</div>
|
|
568
567
|
<TimelineOverlays
|
|
569
|
-
elements={
|
|
570
|
-
elementsRef={
|
|
568
|
+
elements={timelineElements}
|
|
569
|
+
elementsRef={timelineElementsRef}
|
|
571
570
|
theme={theme}
|
|
572
571
|
showShortcutHint={showShortcutHint}
|
|
573
572
|
showPopover={showPopover}
|
|
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
6
6
|
import type { TimelineElement } from "../store/playerStore";
|
|
7
7
|
import { TimelineClip } from "./TimelineClip";
|
|
8
8
|
import type { TimelineEditCapabilities } from "./timelineEditing";
|
|
9
|
+
import { defaultTimelineTheme, type TimelineTheme } from "./timelineTheme";
|
|
9
10
|
|
|
10
11
|
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", {
|
|
11
12
|
configurable: true,
|
|
@@ -27,11 +28,13 @@ function renderClip({
|
|
|
27
28
|
pps = 100,
|
|
28
29
|
isSelected = false,
|
|
29
30
|
hasCustomContent = true,
|
|
31
|
+
theme = defaultTimelineTheme,
|
|
30
32
|
}: {
|
|
31
33
|
element: TimelineElement;
|
|
32
34
|
pps?: number;
|
|
33
35
|
isSelected?: boolean;
|
|
34
36
|
hasCustomContent?: boolean;
|
|
37
|
+
theme?: TimelineTheme;
|
|
35
38
|
}) {
|
|
36
39
|
const host = document.createElement("div");
|
|
37
40
|
document.body.append(host);
|
|
@@ -48,6 +51,7 @@ function renderClip({
|
|
|
48
51
|
isHovered={false}
|
|
49
52
|
hasCustomContent={hasCustomContent}
|
|
50
53
|
capabilities={capabilities}
|
|
54
|
+
theme={theme}
|
|
51
55
|
isComposition={false}
|
|
52
56
|
onHoverStart={vi.fn()}
|
|
53
57
|
onHoverEnd={vi.fn()}
|
|
@@ -115,6 +119,54 @@ describe("TimelineClip", () => {
|
|
|
115
119
|
act(() => root.unmount());
|
|
116
120
|
});
|
|
117
121
|
|
|
122
|
+
it("passes clip and handle theme tokens to the rendered elements", () => {
|
|
123
|
+
const theme: TimelineTheme = {
|
|
124
|
+
...defaultTimelineTheme,
|
|
125
|
+
clipBackground: "var(--test-clip-bg)",
|
|
126
|
+
clipBackgroundActive: "var(--test-clip-bg-active)",
|
|
127
|
+
clipBackgroundHover: "var(--test-clip-bg-hover)",
|
|
128
|
+
clipBackgroundDragging: "var(--test-clip-bg-dragging)",
|
|
129
|
+
clipBorder: "var(--test-clip-border)",
|
|
130
|
+
clipBorderHover: "var(--test-clip-border-hover)",
|
|
131
|
+
clipBorderActive: "var(--test-clip-border-active)",
|
|
132
|
+
handleColor: "var(--test-handle)",
|
|
133
|
+
};
|
|
134
|
+
const { host, root } = renderClip({
|
|
135
|
+
element: { id: "themed", label: "Themed", tag: "div", start: 0, duration: 1, track: 0 },
|
|
136
|
+
isSelected: true,
|
|
137
|
+
theme,
|
|
138
|
+
});
|
|
139
|
+
const clip = host.querySelector<HTMLElement>(".timeline-clip")!;
|
|
140
|
+
expect(clip.style.getPropertyValue("--clip-bg")).toBe("var(--test-clip-bg)");
|
|
141
|
+
expect(clip.style.getPropertyValue("--clip-border-active")).toBe(
|
|
142
|
+
"var(--test-clip-border-active)",
|
|
143
|
+
);
|
|
144
|
+
expect(clip.style.getPropertyValue("--clip-handle")).toBe("var(--test-handle)");
|
|
145
|
+
expect(clip.querySelector<HTMLElement>(".timeline-clip__handle-bar")?.style.background).toBe(
|
|
146
|
+
"var(--clip-handle)",
|
|
147
|
+
);
|
|
148
|
+
act(() => root.unmount());
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("keeps default token references off the properties they resolve", () => {
|
|
152
|
+
const { host, root } = renderClip({
|
|
153
|
+
element: {
|
|
154
|
+
id: "default-theme",
|
|
155
|
+
label: "Default",
|
|
156
|
+
tag: "div",
|
|
157
|
+
start: 0,
|
|
158
|
+
duration: 1,
|
|
159
|
+
track: 0,
|
|
160
|
+
},
|
|
161
|
+
isSelected: true,
|
|
162
|
+
});
|
|
163
|
+
const clip = host.querySelector<HTMLElement>(".timeline-clip")!;
|
|
164
|
+
expect(clip.style.getPropertyValue("--clip-bg")).toBe("var(--timeline-clip-bg)");
|
|
165
|
+
expect(clip.style.getPropertyValue("--clip-bg")).not.toBe("var(--clip-bg)");
|
|
166
|
+
expect(clip.style.getPropertyValue("--clip-handle")).toBe("var(--timeline-handle)");
|
|
167
|
+
act(() => root.unmount());
|
|
168
|
+
});
|
|
169
|
+
|
|
118
170
|
it("is a roving native button with explicit selection semantics", () => {
|
|
119
171
|
const { host, onClick, root } = renderClip({
|
|
120
172
|
element: { id: "hero", label: "Hero", tag: "div", start: 1, duration: 2, track: 0 },
|
|
@@ -64,6 +64,16 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
64
64
|
const showDefaultText = !hasCustomContent && (widthPx >= 40 || isSelected);
|
|
65
65
|
const startLabel = el.start.toFixed(1);
|
|
66
66
|
const endLabel = (el.start + el.duration).toFixed(1);
|
|
67
|
+
const themeVariables = {
|
|
68
|
+
"--clip-bg": theme.clipBackground,
|
|
69
|
+
"--clip-bg-active": theme.clipBackgroundActive,
|
|
70
|
+
"--clip-bg-hover": theme.clipBackgroundHover,
|
|
71
|
+
"--clip-bg-dragging": theme.clipBackgroundDragging,
|
|
72
|
+
"--clip-border": theme.clipBorder,
|
|
73
|
+
"--clip-border-hover": theme.clipBorderHover,
|
|
74
|
+
"--clip-border-active": theme.clipBorderActive,
|
|
75
|
+
"--clip-handle": theme.handleColor,
|
|
76
|
+
} as CSSProperties;
|
|
67
77
|
const clipClassName = [
|
|
68
78
|
"timeline-clip",
|
|
69
79
|
"absolute",
|
|
@@ -81,6 +91,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
81
91
|
top: clipY,
|
|
82
92
|
...(clipHeight === undefined ? { bottom: clipY } : { height: clipHeight }),
|
|
83
93
|
borderRadius: theme.clipRadius,
|
|
94
|
+
...themeVariables,
|
|
84
95
|
zIndex: isDragging ? 20 : isSelected ? 10 : isHovered ? 5 : 1,
|
|
85
96
|
// Regular cursor over clips (CapCut-style, user preference) — no grab hand.
|
|
86
97
|
cursor: "default",
|
|
@@ -144,7 +155,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
144
155
|
bottom: 6,
|
|
145
156
|
width: 2,
|
|
146
157
|
borderRadius: 1,
|
|
147
|
-
background: "
|
|
158
|
+
background: "var(--clip-handle)",
|
|
148
159
|
opacity: handleOpacity * 0.6,
|
|
149
160
|
}}
|
|
150
161
|
/>
|
|
@@ -174,7 +185,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
174
185
|
bottom: 6,
|
|
175
186
|
width: 2,
|
|
176
187
|
borderRadius: 1,
|
|
177
|
-
background: "
|
|
188
|
+
background: "var(--clip-handle)",
|
|
178
189
|
opacity: handleOpacity * 0.6,
|
|
179
190
|
}}
|
|
180
191
|
/>
|
|
@@ -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;
|
|
@@ -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");
|
|
@@ -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
|
*/
|
|
@@ -98,8 +98,8 @@ function buildGroupInfo(
|
|
|
98
98
|
id: groupId,
|
|
99
99
|
label: membership.labelByGroup.get(groupId) ?? groupId,
|
|
100
100
|
// Exactly x.5, which sub-composition child rows are now kept strictly below
|
|
101
|
-
//
|
|
102
|
-
//
|
|
101
|
+
// the timeline row source — they used to be able to land here and collide,
|
|
102
|
+
// duplicating the group header.
|
|
103
103
|
anchorKey: (memberTracks[0] ?? fallbackTrackNum) - 0.5,
|
|
104
104
|
memberTracks,
|
|
105
105
|
memberElements: memberTracks.flatMap((track) => rawByTrack.get(track) ?? []),
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
collectSubCompositionDomChildren,
|
|
6
|
-
collectTopLevelElementIds,
|
|
7
6
|
collectSubCompositionHostState,
|
|
8
7
|
} from "./timelineSyncHydration";
|
|
9
8
|
import type { ClipManifestClip } from "../lib/playbackTypes";
|
|
@@ -95,20 +94,3 @@ describe("collectSubCompositionHostState", () => {
|
|
|
95
94
|
expect(state.size).toBe(0);
|
|
96
95
|
});
|
|
97
96
|
});
|
|
98
|
-
|
|
99
|
-
describe("collectTopLevelElementIds", () => {
|
|
100
|
-
const docOf = (html: string) => new DOMParser().parseFromString(html, "text/html");
|
|
101
|
-
|
|
102
|
-
it("names the sub-composition host and a timed element, not what is nested inside them", () => {
|
|
103
|
-
const doc = docOf(`<div data-composition-id="root">
|
|
104
|
-
<div id="wrap"><div id="a" data-start="0" data-duration="2"><div id="inner" data-start="0"></div></div></div>
|
|
105
|
-
<div id="scene" data-composition-id="scene" data-composition-src="scene.html"><div id="box" data-start="0"></div></div>
|
|
106
|
-
</div>`);
|
|
107
|
-
expect([...collectTopLevelElementIds(doc)!].sort()).toEqual(["a", "scene"]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("is null when the document has no composition root", () => {
|
|
111
|
-
expect(collectTopLevelElementIds(docOf("<p>hi</p>"))).toBeNull();
|
|
112
|
-
expect(collectTopLevelElementIds(null)).toBeNull();
|
|
113
|
-
});
|
|
114
|
-
});
|
|
@@ -14,7 +14,6 @@ import { usePlayerStore } from "../store/playerStore";
|
|
|
14
14
|
import type { TimelineElement, DomClipChild, SubCompositionHostState } from "../store/playerStore";
|
|
15
15
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
16
16
|
import type { ClipTree } from "@hyperframes/core/runtime/clipTree";
|
|
17
|
-
import { topLevelElements, type StructureNode } from "@hyperframes/parsers/top-level-elements";
|
|
18
17
|
import { HF_AUDIO_GROUP_ATTR } from "@hyperframes/core/audio-groups";
|
|
19
18
|
import { groupInfoFor } from "../lib/timelineGroupInfo";
|
|
20
19
|
import type { PlaybackAdapter, ClipManifestClip, IframeWindow } from "../lib/playbackTypes";
|
|
@@ -137,28 +136,6 @@ export function collectSubCompositionDomChildren(
|
|
|
137
136
|
return out;
|
|
138
137
|
}
|
|
139
138
|
|
|
140
|
-
interface DomStructureNode extends StructureNode<DomStructureNode> {
|
|
141
|
-
id: string;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function toStructureNode(el: Element): DomStructureNode {
|
|
145
|
-
const attrs: Record<string, string> = {};
|
|
146
|
-
for (const attr of Array.from(el.attributes)) attrs[attr.name] = attr.value;
|
|
147
|
-
return {
|
|
148
|
-
tag: el.tagName,
|
|
149
|
-
attrs,
|
|
150
|
-
id: el.id,
|
|
151
|
-
children: Array.from(el.children).map(toStructureNode),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** DOM ids of the timeline's rows, by the definition the structure lint shares; null when there is no readable root. */
|
|
156
|
-
export function collectTopLevelElementIds(doc: Document | null): Set<string> | null {
|
|
157
|
-
const root = doc?.querySelector("[data-composition-id]");
|
|
158
|
-
if (!root) return null;
|
|
159
|
-
return new Set(topLevelElements(toStructureNode(root)).flatMap((n) => (n.id ? [n.id] : [])));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
139
|
/** The host-element `data-*` state one element carries, or null when it has none. */
|
|
163
140
|
function readSubCompositionHostState(el: Element): SubCompositionHostState | null {
|
|
164
141
|
const state: SubCompositionHostState = {};
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
buildTimelineElementsFromClips,
|
|
20
20
|
syncManifestTimeline,
|
|
21
21
|
clipTreeParentMap,
|
|
22
|
-
collectTopLevelElementIds,
|
|
23
22
|
collectSubCompositionDomChildren,
|
|
24
23
|
collectSubCompositionHostState,
|
|
25
24
|
hydrateTimelineFromPreview,
|
|
@@ -130,13 +129,11 @@ export function useTimelineSyncCallbacks({
|
|
|
130
129
|
const parentMap = clipTreeParentMap(iframeRef.current?.contentWindow ?? null);
|
|
131
130
|
const domClipChildren = collectSubCompositionDomChildren(iframeDoc, data.clips, parentMap);
|
|
132
131
|
usePlayerStore.getState().setClipParentMap(parentMap);
|
|
133
|
-
usePlayerStore.getState().setTopLevelIds(collectTopLevelElementIds(iframeDoc));
|
|
134
132
|
usePlayerStore.getState().setDomClipChildren(domClipChildren);
|
|
135
133
|
usePlayerStore
|
|
136
134
|
.getState()
|
|
137
135
|
.setSubCompositionHostState(collectSubCompositionHostState(iframeDoc, data.clips));
|
|
138
136
|
} catch {
|
|
139
|
-
usePlayerStore.getState().setTopLevelIds(null);
|
|
140
137
|
// cross-origin or __clipTree not available — maps stay empty
|
|
141
138
|
}
|
|
142
139
|
|
|
@@ -202,8 +202,6 @@ interface PlayerState extends PlayerStoreSlices {
|
|
|
202
202
|
clipManifest: ClipManifestClip[] | null;
|
|
203
203
|
setClipManifest: (clips: ClipManifestClip[] | null) => void;
|
|
204
204
|
clipParentMap: Map<string, string>;
|
|
205
|
-
topLevelIds: ReadonlySet<string> | null;
|
|
206
|
-
setTopLevelIds: (ids: ReadonlySet<string> | null) => void;
|
|
207
205
|
setClipParentMap: (map: Map<string, string>) => void;
|
|
208
206
|
/**
|
|
209
207
|
* Sub-composition DOM descendants (groups + their children) that have no
|
|
@@ -394,8 +392,6 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|
|
394
392
|
setClipManifest: (clips) => set({ clipManifest: clips }),
|
|
395
393
|
clipParentMap: new Map(),
|
|
396
394
|
setClipParentMap: (map) => set({ clipParentMap: map }),
|
|
397
|
-
topLevelIds: null,
|
|
398
|
-
setTopLevelIds: (ids) => set({ topLevelIds: ids }),
|
|
399
395
|
domClipChildren: [],
|
|
400
396
|
setDomClipChildren: (children) => set({ domClipChildren: children }),
|
|
401
397
|
subCompositionHostState: new Map(),
|
|
@@ -77,10 +77,8 @@ export interface TimelineElement {
|
|
|
77
77
|
audioGroupFxChain?: string;
|
|
78
78
|
audioGroupAutomation?: string;
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* lives in. Presence marks the element as expanded; edits subtract it to get
|
|
83
|
-
* the child's local (sourceFile-relative) time. Works at any nesting depth.
|
|
80
|
+
* Legacy source-coordinate marker for an inline sub-composition child. The
|
|
81
|
+
* current timeline keeps sub-compositions as single rows.
|
|
84
82
|
*/
|
|
85
83
|
expandedParentStart?: number;
|
|
86
84
|
expandedHostKey?: string;
|