@hyperframes/studio 0.5.7 → 0.6.0-alpha.10
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/index-14zH9lqh.css +1 -0
- package/dist/assets/index-B-16fRnH.js +108 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +2965 -186
- package/src/components/LintModal.tsx +3 -4
- package/src/components/editor/DomEditOverlay.test.ts +241 -0
- package/src/components/editor/DomEditOverlay.tsx +1300 -0
- package/src/components/editor/MotionPanel.tsx +651 -0
- package/src/components/editor/PropertyPanel.test.ts +116 -0
- package/src/components/editor/PropertyPanel.tsx +2829 -205
- package/src/components/editor/TimelineLayerPanel.test.ts +42 -0
- package/src/components/editor/TimelineLayerPanel.tsx +113 -0
- package/src/components/editor/colorValue.test.ts +82 -0
- package/src/components/editor/colorValue.ts +175 -0
- package/src/components/editor/domEditing.test.ts +1120 -0
- package/src/components/editor/domEditing.ts +1117 -0
- package/src/components/editor/floatingPanel.test.ts +34 -0
- package/src/components/editor/floatingPanel.ts +54 -0
- package/src/components/editor/fontAssets.ts +32 -0
- package/src/components/editor/fontCatalog.ts +126 -0
- package/src/components/editor/gradientValue.test.ts +89 -0
- package/src/components/editor/gradientValue.ts +445 -0
- package/src/components/editor/manualEditingAvailability.test.ts +131 -0
- package/src/components/editor/manualEditingAvailability.ts +62 -0
- package/src/components/editor/manualEdits.test.ts +945 -0
- package/src/components/editor/manualEdits.ts +1409 -0
- package/src/components/editor/manualOffsetDrag.test.ts +140 -0
- package/src/components/editor/manualOffsetDrag.ts +307 -0
- package/src/components/editor/studioMotion.test.ts +355 -0
- package/src/components/editor/studioMotion.ts +632 -0
- package/src/components/nle/NLELayout.test.ts +12 -0
- package/src/components/nle/NLELayout.tsx +84 -22
- package/src/components/nle/NLEPreview.tsx +56 -5
- package/src/components/renders/RenderQueue.tsx +24 -11
- package/src/components/sidebar/AssetsTab.tsx +3 -4
- package/src/components/sidebar/CompositionsTab.test.ts +16 -1
- package/src/components/sidebar/CompositionsTab.tsx +117 -45
- package/src/components/sidebar/LeftSidebar.tsx +194 -179
- package/src/hooks/usePersistentEditHistory.test.ts +256 -0
- package/src/hooks/usePersistentEditHistory.ts +337 -0
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/CompositionThumbnail.test.ts +19 -0
- package/src/player/components/CompositionThumbnail.tsx +50 -13
- package/src/player/components/EditModal.tsx +5 -20
- package/src/player/components/Player.test.ts +58 -0
- package/src/player/components/Player.tsx +88 -5
- package/src/player/components/PlayerControls.tsx +20 -7
- package/src/player/components/Timeline.test.ts +20 -0
- package/src/player/components/Timeline.tsx +147 -40
- package/src/player/components/TimelineClip.test.ts +92 -0
- package/src/player/components/TimelineClip.tsx +241 -7
- package/src/player/components/timelineEditing.test.ts +16 -3
- package/src/player/components/timelineEditing.ts +10 -3
- package/src/player/hooks/useTimelinePlayer.test.ts +148 -19
- package/src/player/hooks/useTimelinePlayer.ts +287 -16
- package/src/player/store/playerStore.ts +2 -0
- package/src/utils/clipboard.test.ts +89 -0
- package/src/utils/clipboard.ts +57 -0
- package/src/utils/editHistory.test.ts +244 -0
- package/src/utils/editHistory.ts +218 -0
- package/src/utils/editHistoryStorage.test.ts +37 -0
- package/src/utils/editHistoryStorage.ts +99 -0
- package/src/utils/mediaTypes.ts +1 -1
- package/src/utils/sourcePatcher.test.ts +128 -1
- package/src/utils/sourcePatcher.ts +130 -18
- package/src/utils/studioFileHistory.test.ts +156 -0
- package/src/utils/studioFileHistory.ts +61 -0
- package/src/utils/timelineAssetDrop.test.ts +31 -11
- package/src/utils/timelineAssetDrop.ts +22 -2
- package/src/utils/timelineDiscovery.ts +1 -1
- package/src/utils/timelineInspector.test.ts +79 -0
- package/src/utils/timelineInspector.ts +116 -0
- package/dist/assets/index-04Mp2wOn.css +0 -1
- package/dist/assets/index-Dcw3BoVw.js +0 -93
|
@@ -10,13 +10,19 @@ interface PlayerProps {
|
|
|
10
10
|
projectId?: string;
|
|
11
11
|
directUrl?: string;
|
|
12
12
|
onLoad: () => void;
|
|
13
|
+
onCompositionLoadingChange?: (loading: boolean) => void;
|
|
13
14
|
portrait?: boolean;
|
|
15
|
+
style?: React.CSSProperties;
|
|
16
|
+
suppressLoadingOverlay?: boolean;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
interface HyperframesPlayerElement extends HTMLElement {
|
|
17
20
|
iframeElement: HTMLIFrameElement;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
const MEDIA_HAVE_FUTURE_DATA = 3;
|
|
24
|
+
const MEDIA_NETWORK_NO_SOURCE = 3;
|
|
25
|
+
|
|
20
26
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
21
27
|
return typeof value === "object" && value !== null;
|
|
22
28
|
}
|
|
@@ -30,6 +36,26 @@ function getShaderTransitionLoading(event: Event): boolean | null {
|
|
|
30
36
|
return state.loading === true && state.ready !== true;
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
export function shouldShowCompositionLoadingOverlay(compositionLoading: boolean): boolean {
|
|
40
|
+
return compositionLoading;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function enableInteractiveIframe(player: HyperframesPlayerElement): void {
|
|
44
|
+
const root = player.shadowRoot;
|
|
45
|
+
if (!root) return;
|
|
46
|
+
|
|
47
|
+
const container = root.querySelector<HTMLElement>(".hfp-container");
|
|
48
|
+
const iframe = root.querySelector<HTMLIFrameElement>(".hfp-iframe");
|
|
49
|
+
|
|
50
|
+
container?.style.setProperty("pointer-events", "auto");
|
|
51
|
+
iframe?.style.setProperty("pointer-events", "auto");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isPreviewMediaElement(el: Element): el is HTMLMediaElement {
|
|
55
|
+
const tagName = el.tagName.toLowerCase();
|
|
56
|
+
return tagName === "video" || tagName === "audio";
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
// Assets are considered ready when every `<video>`/`<audio>` has enough data
|
|
34
60
|
// to play through without buffering, and every registered Lottie animation has
|
|
35
61
|
// finished loading.
|
|
@@ -38,14 +64,19 @@ function getShaderTransitionLoading(event: Event): boolean | null {
|
|
|
38
64
|
// races so a brief access failure (e.g. an iframe that just swapped src)
|
|
39
65
|
// doesn't flicker the overlay state — we keep showing whatever was most
|
|
40
66
|
// recently true.
|
|
41
|
-
function hasUnloadedAssets(iframe: HTMLIFrameElement, lastResult: boolean): boolean {
|
|
67
|
+
export function hasUnloadedAssets(iframe: HTMLIFrameElement, lastResult: boolean): boolean {
|
|
42
68
|
try {
|
|
43
69
|
const win = iframe.contentWindow as unknown as (Window & { __hfLottie?: unknown[] }) | null;
|
|
44
70
|
const doc = iframe.contentDocument;
|
|
45
71
|
if (!win || !doc) return lastResult;
|
|
46
72
|
|
|
47
73
|
for (const el of doc.querySelectorAll("video, audio")) {
|
|
48
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
isPreviewMediaElement(el) &&
|
|
76
|
+
!el.error &&
|
|
77
|
+
el.networkState !== MEDIA_NETWORK_NO_SOURCE &&
|
|
78
|
+
el.readyState < MEDIA_HAVE_FUTURE_DATA
|
|
79
|
+
) {
|
|
49
80
|
return true;
|
|
50
81
|
}
|
|
51
82
|
}
|
|
@@ -72,7 +103,18 @@ function hasUnloadedAssets(iframe: HTMLIFrameElement, lastResult: boolean): bool
|
|
|
72
103
|
* timeline probing, and DOM inspection.
|
|
73
104
|
*/
|
|
74
105
|
export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
75
|
-
(
|
|
106
|
+
(
|
|
107
|
+
{
|
|
108
|
+
projectId,
|
|
109
|
+
directUrl,
|
|
110
|
+
onLoad,
|
|
111
|
+
onCompositionLoadingChange,
|
|
112
|
+
portrait,
|
|
113
|
+
style,
|
|
114
|
+
suppressLoadingOverlay,
|
|
115
|
+
},
|
|
116
|
+
ref,
|
|
117
|
+
) => {
|
|
76
118
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
77
119
|
const loadCountRef = useRef(0);
|
|
78
120
|
const assetPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
@@ -81,6 +123,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
81
123
|
const [assetOverlayVisible, setAssetOverlayVisible] = useState(false);
|
|
82
124
|
const [assetOverlayFading, setAssetOverlayFading] = useState(false);
|
|
83
125
|
const [shaderTransitionLoading, setShaderTransitionLoading] = useState(false);
|
|
126
|
+
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
84
127
|
|
|
85
128
|
useMountEffect(() => {
|
|
86
129
|
const container = containerRef.current;
|
|
@@ -105,6 +148,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
105
148
|
player.style.height = "100%";
|
|
106
149
|
player.style.display = "block";
|
|
107
150
|
container.appendChild(player);
|
|
151
|
+
enableInteractiveIframe(player);
|
|
108
152
|
|
|
109
153
|
// Bridge the inner iframe to the forwarded ref for useTimelinePlayer.
|
|
110
154
|
const iframe = player.iframeElement;
|
|
@@ -125,10 +169,20 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
125
169
|
};
|
|
126
170
|
player.addEventListener("shadertransitionstate", handleShaderTransitionState);
|
|
127
171
|
|
|
172
|
+
const handleReady = () => {
|
|
173
|
+
setCompositionLoading(false);
|
|
174
|
+
};
|
|
175
|
+
const handleError = () => {
|
|
176
|
+
setCompositionLoading(false);
|
|
177
|
+
};
|
|
178
|
+
player.addEventListener("ready", handleReady);
|
|
179
|
+
player.addEventListener("error", handleError);
|
|
180
|
+
|
|
128
181
|
// Forward the iframe's native load event to the studio's onIframeLoad.
|
|
129
182
|
const handleLoad = () => {
|
|
130
183
|
loadCountRef.current++;
|
|
131
184
|
setShaderTransitionLoading(false);
|
|
185
|
+
setCompositionLoading(true);
|
|
132
186
|
// Reveal animation on reload (hot-reload, composition switch)
|
|
133
187
|
if (loadCountRef.current > 1) {
|
|
134
188
|
container.classList.remove("preview-revealing");
|
|
@@ -179,6 +233,8 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
179
233
|
iframe.removeEventListener("load", handleLoad);
|
|
180
234
|
player.removeEventListener("click", preventToggle, { capture: true });
|
|
181
235
|
player.removeEventListener("shadertransitionstate", handleShaderTransitionState);
|
|
236
|
+
player.removeEventListener("ready", handleReady);
|
|
237
|
+
player.removeEventListener("error", handleError);
|
|
182
238
|
if (assetPollRef.current) clearInterval(assetPollRef.current);
|
|
183
239
|
assetPollRef.current = null;
|
|
184
240
|
container.removeChild(player);
|
|
@@ -224,11 +280,38 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
224
280
|
};
|
|
225
281
|
}, [assetsLoading]);
|
|
226
282
|
|
|
227
|
-
const
|
|
283
|
+
const showCompositionOverlay =
|
|
284
|
+
!suppressLoadingOverlay && shouldShowCompositionLoadingOverlay(compositionLoading);
|
|
285
|
+
const showAssetOverlay =
|
|
286
|
+
assetOverlayVisible && !shaderTransitionLoading && !showCompositionOverlay;
|
|
287
|
+
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
onCompositionLoadingChange?.(showCompositionOverlay || showAssetOverlay);
|
|
290
|
+
}, [onCompositionLoadingChange, showCompositionOverlay, showAssetOverlay]);
|
|
228
291
|
|
|
229
292
|
return (
|
|
230
|
-
<div
|
|
293
|
+
<div
|
|
294
|
+
className="relative w-full h-full max-w-full max-h-full overflow-hidden bg-black flex items-center justify-center"
|
|
295
|
+
style={style}
|
|
296
|
+
>
|
|
231
297
|
<div ref={containerRef} className="w-full h-full" />
|
|
298
|
+
{showCompositionOverlay && (
|
|
299
|
+
<div
|
|
300
|
+
className="absolute inset-0 bg-black flex items-center justify-center z-30 select-none"
|
|
301
|
+
data-hyperframes-ignore=""
|
|
302
|
+
data-testid="composition-loading-overlay"
|
|
303
|
+
draggable={false}
|
|
304
|
+
onDragStart={(event) => event.preventDefault()}
|
|
305
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
306
|
+
onPointerDown={(event) => event.preventDefault()}
|
|
307
|
+
>
|
|
308
|
+
<HyperframesLoader
|
|
309
|
+
title="Loading composition"
|
|
310
|
+
detail="Preparing the Studio preview."
|
|
311
|
+
size={56}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
232
315
|
{showAssetOverlay && (
|
|
233
316
|
<div
|
|
234
317
|
className="absolute inset-0 bg-black flex items-center justify-center z-20 select-none"
|
|
@@ -26,11 +26,13 @@ export function resolveSeekPercent(clientX: number, rectLeft: number, rectWidth:
|
|
|
26
26
|
interface PlayerControlsProps {
|
|
27
27
|
onTogglePlay: () => void;
|
|
28
28
|
onSeek: (time: number) => void;
|
|
29
|
+
disabled?: boolean;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export const PlayerControls = memo(function PlayerControls({
|
|
32
33
|
onTogglePlay,
|
|
33
34
|
onSeek,
|
|
35
|
+
disabled = false,
|
|
34
36
|
}: PlayerControlsProps) {
|
|
35
37
|
// Subscribe to only the fields we render — each selector prevents cascading re-renders
|
|
36
38
|
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
@@ -57,6 +59,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
57
59
|
|
|
58
60
|
const durationRef = useRef(duration);
|
|
59
61
|
durationRef.current = duration;
|
|
62
|
+
const controlsDisabled = disabled || !timelineReady;
|
|
60
63
|
useMountEffect(() => {
|
|
61
64
|
const updateProgress = (t: number) => {
|
|
62
65
|
currentTimeRef.current = t;
|
|
@@ -115,6 +118,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
115
118
|
|
|
116
119
|
const seekFromClientX = useCallback(
|
|
117
120
|
(clientX: number) => {
|
|
121
|
+
if (disabled) return;
|
|
118
122
|
const bar = seekBarRef.current;
|
|
119
123
|
if (!bar || duration <= 0) return;
|
|
120
124
|
const rect = bar.getBoundingClientRect();
|
|
@@ -125,7 +129,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
125
129
|
if (progressThumbRef.current) progressThumbRef.current.style.left = `${pct}%`;
|
|
126
130
|
onSeek(percent * duration);
|
|
127
131
|
},
|
|
128
|
-
[duration, onSeek],
|
|
132
|
+
[disabled, duration, onSeek],
|
|
129
133
|
);
|
|
130
134
|
|
|
131
135
|
const handlePointerDown = useCallback(
|
|
@@ -204,7 +208,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
204
208
|
|
|
205
209
|
const handleKeyDown = useCallback(
|
|
206
210
|
(e: React.KeyboardEvent) => {
|
|
207
|
-
if (!timelineReady || duration <= 0) return;
|
|
211
|
+
if (disabled || !timelineReady || duration <= 0) return;
|
|
208
212
|
const step = e.shiftKey ? 10 : 1;
|
|
209
213
|
if (e.key === "ArrowLeft") {
|
|
210
214
|
e.preventDefault();
|
|
@@ -214,14 +218,15 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
214
218
|
onSeek(Math.min(duration, stepFrameTime(currentTimeRef.current, step)));
|
|
215
219
|
}
|
|
216
220
|
},
|
|
217
|
-
[timelineReady, duration, onSeek],
|
|
221
|
+
[disabled, timelineReady, duration, onSeek],
|
|
218
222
|
);
|
|
219
223
|
|
|
220
224
|
const commitJumpFrame = useCallback(() => {
|
|
225
|
+
if (disabled) return;
|
|
221
226
|
const frame = Number.parseInt(jumpFrame, 10);
|
|
222
227
|
if (!Number.isFinite(frame) || duration <= 0) return;
|
|
223
228
|
onSeek(Math.min(duration, frameToSeconds(Math.max(0, frame))));
|
|
224
|
-
}, [duration, jumpFrame, onSeek]);
|
|
229
|
+
}, [disabled, duration, jumpFrame, onSeek]);
|
|
225
230
|
|
|
226
231
|
const handleJumpSubmit = useCallback(
|
|
227
232
|
(e: React.FormEvent) => {
|
|
@@ -243,6 +248,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
243
248
|
return (
|
|
244
249
|
<div
|
|
245
250
|
className="px-4 py-2 flex flex-wrap items-center gap-x-2 gap-y-1"
|
|
251
|
+
aria-disabled={disabled || undefined}
|
|
246
252
|
style={{
|
|
247
253
|
borderTop: "1px solid rgba(255,255,255,0.04)",
|
|
248
254
|
// Add iOS safe-area inset so Safari's bottom URL bar doesn't occlude
|
|
@@ -256,7 +262,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
256
262
|
type="button"
|
|
257
263
|
aria-label={isPlaying ? "Pause" : "Play"}
|
|
258
264
|
onClick={onTogglePlay}
|
|
259
|
-
disabled={
|
|
265
|
+
disabled={controlsDisabled}
|
|
260
266
|
className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg disabled:opacity-30 disabled:pointer-events-none transition-colors"
|
|
261
267
|
style={{ background: "rgba(255,255,255,0.06)" }}
|
|
262
268
|
>
|
|
@@ -293,12 +299,15 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
293
299
|
(sliderRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
|
294
300
|
}}
|
|
295
301
|
role="slider"
|
|
296
|
-
tabIndex={0}
|
|
302
|
+
tabIndex={disabled ? -1 : 0}
|
|
297
303
|
aria-label="Seek"
|
|
304
|
+
aria-disabled={disabled || undefined}
|
|
298
305
|
aria-valuemin={0}
|
|
299
306
|
aria-valuemax={Math.round(duration)}
|
|
300
307
|
aria-valuenow={0}
|
|
301
|
-
className=
|
|
308
|
+
className={`min-w-[96px] flex-1 h-6 flex items-center group ${
|
|
309
|
+
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"
|
|
310
|
+
}`}
|
|
302
311
|
// `touch-action: none` tells the browser we're handling every
|
|
303
312
|
// pointer gesture on this element ourselves. Without it, iOS
|
|
304
313
|
// Safari consumes horizontal swipes for its own swipe-back-to-
|
|
@@ -334,6 +343,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
334
343
|
<button
|
|
335
344
|
type="button"
|
|
336
345
|
onClick={() => setShowSpeedMenu((v) => !v)}
|
|
346
|
+
disabled={disabled}
|
|
337
347
|
className="w-10 px-2 py-1 rounded-md text-[10px] font-mono tabular-nums transition-colors"
|
|
338
348
|
style={{ color: "#71717A", background: "rgba(255,255,255,0.04)" }}
|
|
339
349
|
>
|
|
@@ -374,6 +384,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
374
384
|
<button
|
|
375
385
|
type="button"
|
|
376
386
|
onClick={() => setLoopEnabled(!loopEnabled)}
|
|
387
|
+
disabled={disabled}
|
|
377
388
|
className={`h-7 w-14 rounded-md border px-2 text-[10px] font-medium transition-colors ${
|
|
378
389
|
loopEnabled
|
|
379
390
|
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
@@ -389,6 +400,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
389
400
|
<button
|
|
390
401
|
type="button"
|
|
391
402
|
onClick={() => setTimeDisplayMode((mode) => (mode === "time" ? "frame" : "time"))}
|
|
403
|
+
disabled={disabled}
|
|
392
404
|
className="h-7 w-14 rounded-md border border-neutral-700 px-2 text-[10px] font-mono text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800"
|
|
393
405
|
title="Toggle time/frame display"
|
|
394
406
|
aria-label="Toggle time and frame display"
|
|
@@ -403,6 +415,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
403
415
|
<input
|
|
404
416
|
value={jumpFrame}
|
|
405
417
|
onChange={(e) => setJumpFrame(e.target.value)}
|
|
418
|
+
disabled={disabled}
|
|
406
419
|
inputMode="numeric"
|
|
407
420
|
pattern="[0-9]*"
|
|
408
421
|
aria-label="Jump to frame"
|
|
@@ -8,9 +8,12 @@ import {
|
|
|
8
8
|
getTimelinePlayheadLeft,
|
|
9
9
|
getTimelineScrollLeftForZoomAnchor,
|
|
10
10
|
getTimelineScrollLeftForZoomTransition,
|
|
11
|
+
shouldShowTimelineShortcutHint,
|
|
11
12
|
shouldHandleTimelineDeleteKey,
|
|
12
13
|
shouldAutoScrollTimeline,
|
|
13
14
|
} from "./Timeline";
|
|
15
|
+
import { TIMELINE_CLIP_CONTROL_Z_INDEX } from "./TimelineClip";
|
|
16
|
+
import { COMPOSITION_THUMBNAIL_LABEL_Z_INDEX } from "./CompositionThumbnail";
|
|
14
17
|
import { formatTime } from "../lib/time";
|
|
15
18
|
|
|
16
19
|
describe("generateTicks", () => {
|
|
@@ -163,6 +166,12 @@ describe("shouldAutoScrollTimeline", () => {
|
|
|
163
166
|
});
|
|
164
167
|
});
|
|
165
168
|
|
|
169
|
+
describe("timeline clip controls", () => {
|
|
170
|
+
it("renders layer controls above composition thumbnail chrome", () => {
|
|
171
|
+
expect(TIMELINE_CLIP_CONTROL_Z_INDEX).toBeGreaterThan(COMPOSITION_THUMBNAIL_LABEL_Z_INDEX);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
166
175
|
describe("getTimelineScrollLeftForZoomTransition", () => {
|
|
167
176
|
it("resets horizontal scroll when switching from manual zoom back to fit", () => {
|
|
168
177
|
expect(getTimelineScrollLeftForZoomTransition("manual", "fit", 480)).toBe(0);
|
|
@@ -237,6 +246,17 @@ describe("getTimelineCanvasHeight", () => {
|
|
|
237
246
|
});
|
|
238
247
|
});
|
|
239
248
|
|
|
249
|
+
describe("shouldShowTimelineShortcutHint", () => {
|
|
250
|
+
it("shows the hint when the timeline does not vertically overflow", () => {
|
|
251
|
+
expect(shouldShowTimelineShortcutHint(220, 220)).toBe(true);
|
|
252
|
+
expect(shouldShowTimelineShortcutHint(220.5, 220)).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("hides the hint when timeline tracks need vertical scrolling", () => {
|
|
256
|
+
expect(shouldShowTimelineShortcutHint(221.5, 220)).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
240
260
|
describe("shouldHandleTimelineDeleteKey", () => {
|
|
241
261
|
it("handles Delete and Backspace when focus is not in an editor", () => {
|
|
242
262
|
expect(shouldHandleTimelineDeleteKey({ key: "Delete" })).toBe(true);
|