@hyperframes/studio 0.6.0-alpha.12 → 0.6.0-alpha.14

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.
Files changed (61) hide show
  1. package/dist/assets/hyperframes-player-BI1oj9hu.js +418 -0
  2. package/dist/assets/index-CBj2NLRG.js +117 -0
  3. package/dist/assets/index-D1JDq7Gg.css +1 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +427 -4487
  8. package/src/components/AskAgentModal.tsx +120 -0
  9. package/src/components/StudioHeader.tsx +133 -0
  10. package/src/components/StudioLeftSidebar.tsx +125 -0
  11. package/src/components/StudioPreviewArea.tsx +163 -0
  12. package/src/components/StudioRightPanel.tsx +198 -0
  13. package/src/components/TimelineToolbar.tsx +89 -0
  14. package/src/components/editor/DomEditOverlay.tsx +15 -1
  15. package/src/components/editor/PropertyPanel.tsx +129 -2681
  16. package/src/components/editor/domEditing.ts +38 -5
  17. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  18. package/src/components/editor/manualEditingAvailability.ts +1 -1
  19. package/src/components/editor/manualEdits.ts +32 -0
  20. package/src/components/editor/propertyPanelColor.tsx +371 -0
  21. package/src/components/editor/propertyPanelFill.tsx +421 -0
  22. package/src/components/editor/propertyPanelFont.tsx +455 -0
  23. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  24. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  25. package/src/components/editor/propertyPanelSections.tsx +453 -0
  26. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  27. package/src/components/nle/NLELayout.tsx +0 -10
  28. package/src/contexts/DomEditContext.tsx +137 -0
  29. package/src/contexts/FileManagerContext.tsx +110 -0
  30. package/src/contexts/PanelLayoutContext.tsx +68 -0
  31. package/src/contexts/StudioContext.tsx +135 -0
  32. package/src/hooks/useAppHotkeys.ts +326 -0
  33. package/src/hooks/useAskAgentModal.ts +162 -0
  34. package/src/hooks/useCaptionDetection.ts +132 -0
  35. package/src/hooks/useCompositionDimensions.ts +25 -0
  36. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  37. package/src/hooks/useDomEditCommits.ts +437 -0
  38. package/src/hooks/useDomEditSession.ts +342 -0
  39. package/src/hooks/useDomEditTextCommits.ts +330 -0
  40. package/src/hooks/useDomSelection.ts +398 -0
  41. package/src/hooks/useFileManager.ts +431 -0
  42. package/src/hooks/useFrameCapture.ts +77 -0
  43. package/src/hooks/useLintModal.ts +35 -0
  44. package/src/hooks/useManifestPersistence.ts +492 -0
  45. package/src/hooks/usePanelLayout.ts +68 -0
  46. package/src/hooks/usePreviewInteraction.ts +153 -0
  47. package/src/hooks/useRenderClipContent.ts +124 -0
  48. package/src/hooks/useTimelineEditing.ts +472 -0
  49. package/src/player/components/Player.tsx +19 -1
  50. package/src/player/components/Timeline.test.ts +0 -8
  51. package/src/player/components/Timeline.tsx +2 -83
  52. package/src/player/components/TimelineClip.tsx +9 -244
  53. package/src/player/hooks/useTimelinePlayer.ts +140 -103
  54. package/src/utils/domEditHelpers.ts +50 -0
  55. package/src/utils/studioFontHelpers.ts +83 -0
  56. package/src/utils/studioHelpers.ts +214 -0
  57. package/src/utils/studioPreviewHelpers.ts +185 -0
  58. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  59. package/dist/assets/index-FWg79aJz.css +0 -1
  60. package/dist/assets/index-xdyn_qRZ.js +0 -110
  61. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,472 @@
1
+ import { useCallback, useRef } from "react";
2
+ import type { TimelineElement } from "../player";
3
+ import { usePlayerStore } from "../player";
4
+ import { applyPatchByTarget, readAttributeByTarget } from "../utils/sourcePatcher";
5
+ import {
6
+ buildTrackZIndexMap,
7
+ formatTimelineAttributeNumber,
8
+ } from "../player/components/timelineEditing";
9
+ import {
10
+ buildTimelineAssetId,
11
+ buildTimelineAssetInsertHtml,
12
+ buildTimelineFileDropPlacements,
13
+ getTimelineAssetKind,
14
+ insertTimelineAssetIntoSource,
15
+ resolveTimelineAssetInitialGeometry,
16
+ resolveTimelineAssetSrc,
17
+ } from "../utils/timelineAssetDrop";
18
+ import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
19
+ import {
20
+ getTimelineElementLabel,
21
+ collectHtmlIds,
22
+ resolveDroppedAssetDuration,
23
+ } from "../utils/studioHelpers";
24
+ import type { EditHistoryKind } from "../utils/editHistory";
25
+
26
+ // ── Types ──
27
+
28
+ interface RecordEditInput {
29
+ label: string;
30
+ kind: EditHistoryKind;
31
+ coalesceKey?: string;
32
+ files: Record<string, { before: string; after: string }>;
33
+ }
34
+
35
+ interface UseTimelineEditingOptions {
36
+ projectId: string | null;
37
+ activeCompPath: string | null;
38
+ timelineElements: TimelineElement[];
39
+ showToast: (message: string, tone?: "error" | "info") => void;
40
+ writeProjectFile: (path: string, content: string) => Promise<void>;
41
+ recordEdit: (input: RecordEditInput) => Promise<void>;
42
+ domEditSaveTimestampRef: React.MutableRefObject<number>;
43
+ reloadPreview: () => void;
44
+ uploadProjectFiles: (files: Iterable<File>, dir?: string) => Promise<string[]>;
45
+ }
46
+
47
+ // ── Helpers ──
48
+
49
+ function buildPatchTarget(element: { domId?: string; selector?: string; selectorIndex?: number }) {
50
+ if (element.domId) {
51
+ return { id: element.domId, selector: element.selector, selectorIndex: element.selectorIndex };
52
+ }
53
+ if (element.selector) {
54
+ return { selector: element.selector, selectorIndex: element.selectorIndex };
55
+ }
56
+ return null;
57
+ }
58
+
59
+ async function readFileContent(projectId: string, targetPath: string): Promise<string> {
60
+ const response = await fetch(
61
+ `/api/projects/${projectId}/files/${encodeURIComponent(targetPath)}`,
62
+ );
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to read ${targetPath}`);
65
+ }
66
+ const data = (await response.json()) as { content?: string };
67
+ if (typeof data.content !== "string") {
68
+ throw new Error(`Missing file contents for ${targetPath}`);
69
+ }
70
+ return data.content;
71
+ }
72
+
73
+ // ── Hook ──
74
+
75
+ export function useTimelineEditing({
76
+ projectId,
77
+ activeCompPath,
78
+ timelineElements,
79
+ showToast,
80
+ writeProjectFile,
81
+ recordEdit,
82
+ domEditSaveTimestampRef,
83
+ reloadPreview,
84
+ uploadProjectFiles,
85
+ }: UseTimelineEditingOptions) {
86
+ const projectIdRef = useRef(projectId);
87
+ projectIdRef.current = projectId;
88
+
89
+ const lastBlockedTimelineToastAtRef = useRef(0);
90
+
91
+ const handleTimelineElementMove = useCallback(
92
+ async (element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => {
93
+ const pid = projectIdRef.current;
94
+ if (!pid) throw new Error("No active project");
95
+
96
+ const targetPath = element.sourceFile || activeCompPath || "index.html";
97
+ const originalContent = await readFileContent(pid, targetPath);
98
+
99
+ const patchTarget = buildPatchTarget(element);
100
+ if (!patchTarget) {
101
+ throw new Error(`Timeline element ${element.id} is missing a patchable target`);
102
+ }
103
+
104
+ const resolvedTargetPath = targetPath || "index.html";
105
+ const relevantElements = timelineElements
106
+ .map((te) =>
107
+ (te.key ?? te.id) === (element.key ?? element.id)
108
+ ? { ...te, start: updates.start, track: updates.track }
109
+ : te,
110
+ )
111
+ .filter((te) => (te.sourceFile || activeCompPath || "index.html") === resolvedTargetPath);
112
+ const trackZIndices = buildTrackZIndexMap(relevantElements.map((te) => te.track));
113
+
114
+ let patchedContent = applyPatchByTarget(originalContent, patchTarget, {
115
+ type: "attribute",
116
+ property: "start",
117
+ value: formatTimelineAttributeNumber(updates.start),
118
+ });
119
+ patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
120
+ type: "attribute",
121
+ property: "track-index",
122
+ value: String(updates.track),
123
+ });
124
+ for (const te of relevantElements) {
125
+ const elementTarget = buildPatchTarget(te);
126
+ if (!elementTarget) continue;
127
+ const nextZIndex = trackZIndices.get(te.track);
128
+ if (nextZIndex == null) continue;
129
+ patchedContent = applyPatchByTarget(patchedContent, elementTarget, {
130
+ type: "inline-style",
131
+ property: "z-index",
132
+ value: String(nextZIndex),
133
+ });
134
+ }
135
+
136
+ if (patchedContent === originalContent) {
137
+ throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
138
+ }
139
+
140
+ domEditSaveTimestampRef.current = Date.now();
141
+ await saveProjectFilesWithHistory({
142
+ projectId: pid,
143
+ label: "Move timeline clip",
144
+ kind: "timeline",
145
+ files: { [targetPath]: patchedContent },
146
+ readFile: async () => originalContent,
147
+ writeFile: writeProjectFile,
148
+ recordEdit,
149
+ });
150
+
151
+ reloadPreview();
152
+ },
153
+ [
154
+ activeCompPath,
155
+ recordEdit,
156
+ timelineElements,
157
+ writeProjectFile,
158
+ domEditSaveTimestampRef,
159
+ reloadPreview,
160
+ ],
161
+ );
162
+
163
+ const handleTimelineElementResize = useCallback(
164
+ async (
165
+ element: TimelineElement,
166
+ updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
167
+ ) => {
168
+ const pid = projectIdRef.current;
169
+ if (!pid) throw new Error("No active project");
170
+
171
+ const targetPath = element.sourceFile || activeCompPath || "index.html";
172
+ const originalContent = await readFileContent(pid, targetPath);
173
+
174
+ const patchTarget = buildPatchTarget(element);
175
+ if (!patchTarget) {
176
+ throw new Error(`Timeline element ${element.id} is missing a patchable target`);
177
+ }
178
+
179
+ const playbackStartAttrName =
180
+ element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
181
+ const currentPlaybackStartValue =
182
+ readAttributeByTarget(originalContent, patchTarget, "playback-start") ??
183
+ readAttributeByTarget(originalContent, patchTarget, "media-start");
184
+ const currentPlaybackStart =
185
+ currentPlaybackStartValue != null ? parseFloat(currentPlaybackStartValue) : undefined;
186
+ const trimDelta = updates.start - element.start;
187
+ const fallbackPlaybackStart =
188
+ updates.playbackStart == null &&
189
+ trimDelta !== 0 &&
190
+ Number.isFinite(currentPlaybackStart) &&
191
+ currentPlaybackStart != null
192
+ ? Math.max(0, currentPlaybackStart + trimDelta * Math.max(element.playbackRate ?? 1, 0.1))
193
+ : undefined;
194
+ const nextPlaybackStart = updates.playbackStart ?? fallbackPlaybackStart;
195
+
196
+ let patchedContent = originalContent;
197
+ patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
198
+ type: "attribute",
199
+ property: "start",
200
+ value: formatTimelineAttributeNumber(updates.start),
201
+ });
202
+ patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
203
+ type: "attribute",
204
+ property: "duration",
205
+ value: formatTimelineAttributeNumber(updates.duration),
206
+ });
207
+ if (nextPlaybackStart != null) {
208
+ patchedContent = applyPatchByTarget(patchedContent, patchTarget, {
209
+ type: "attribute",
210
+ property: playbackStartAttrName,
211
+ value: formatTimelineAttributeNumber(nextPlaybackStart),
212
+ });
213
+ }
214
+
215
+ if (patchedContent === originalContent) {
216
+ throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
217
+ }
218
+
219
+ domEditSaveTimestampRef.current = Date.now();
220
+ await saveProjectFilesWithHistory({
221
+ projectId: pid,
222
+ label: "Resize timeline clip",
223
+ kind: "timeline",
224
+ files: { [targetPath]: patchedContent },
225
+ readFile: async () => originalContent,
226
+ writeFile: writeProjectFile,
227
+ recordEdit,
228
+ });
229
+
230
+ reloadPreview();
231
+ },
232
+ [activeCompPath, recordEdit, writeProjectFile, domEditSaveTimestampRef, reloadPreview],
233
+ );
234
+
235
+ const handleTimelineElementDelete = useCallback(
236
+ async (element: TimelineElement) => {
237
+ const pid = projectIdRef.current;
238
+ if (!pid) throw new Error("No active project");
239
+ const label = getTimelineElementLabel(element);
240
+
241
+ const targetPath = element.sourceFile || activeCompPath || "index.html";
242
+ try {
243
+ const originalContent = await readFileContent(pid, targetPath);
244
+
245
+ const patchTarget = buildPatchTarget(element);
246
+ if (!patchTarget) {
247
+ throw new Error(`Timeline element ${element.id} is missing a patchable target`);
248
+ }
249
+
250
+ const resolvedTargetPath = targetPath || "index.html";
251
+ const remainingElements = timelineElements.filter(
252
+ (te) =>
253
+ (te.key ?? te.id) !== (element.key ?? element.id) &&
254
+ (te.sourceFile || activeCompPath || "index.html") === resolvedTargetPath,
255
+ );
256
+ const trackZIndices = buildTrackZIndexMap(remainingElements.map((te) => te.track));
257
+
258
+ const removeResponse = await fetch(
259
+ `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
260
+ {
261
+ method: "POST",
262
+ headers: { "Content-Type": "application/json" },
263
+ body: JSON.stringify({ target: patchTarget }),
264
+ },
265
+ );
266
+ if (!removeResponse.ok) {
267
+ throw new Error(`Failed to delete ${element.id} from ${targetPath}`);
268
+ }
269
+
270
+ const removeData = (await removeResponse.json()) as {
271
+ changed?: boolean;
272
+ content?: string;
273
+ };
274
+ let patchedContent =
275
+ typeof removeData.content === "string" ? removeData.content : originalContent;
276
+ for (const te of remainingElements) {
277
+ const elementTarget = buildPatchTarget(te);
278
+ if (!elementTarget) continue;
279
+ const nextZIndex = trackZIndices.get(te.track);
280
+ if (nextZIndex == null) continue;
281
+ patchedContent = applyPatchByTarget(patchedContent, elementTarget, {
282
+ type: "inline-style",
283
+ property: "z-index",
284
+ value: String(nextZIndex),
285
+ });
286
+ }
287
+
288
+ domEditSaveTimestampRef.current = Date.now();
289
+ await saveProjectFilesWithHistory({
290
+ projectId: pid,
291
+ label: "Delete timeline clip",
292
+ kind: "timeline",
293
+ files: { [targetPath]: patchedContent },
294
+ readFile: async () => originalContent,
295
+ writeFile: writeProjectFile,
296
+ recordEdit,
297
+ });
298
+
299
+ usePlayerStore
300
+ .getState()
301
+ .setElements(
302
+ timelineElements.filter((te) => (te.key ?? te.id) !== (element.key ?? element.id)),
303
+ );
304
+ usePlayerStore.getState().setSelectedElementId(null);
305
+ reloadPreview();
306
+ showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
307
+ } catch (error) {
308
+ const message = error instanceof Error ? error.message : "Failed to delete timeline clip";
309
+ showToast(message);
310
+ }
311
+ },
312
+ [
313
+ activeCompPath,
314
+ recordEdit,
315
+ showToast,
316
+ timelineElements,
317
+ writeProjectFile,
318
+ domEditSaveTimestampRef,
319
+ reloadPreview,
320
+ ],
321
+ );
322
+
323
+ const handleTimelineAssetDrop = useCallback(
324
+ async (
325
+ assetPath: string,
326
+ placement: Pick<TimelineElement, "start" | "track">,
327
+ durationOverride?: number,
328
+ ) => {
329
+ const pid = projectIdRef.current;
330
+ if (!pid) throw new Error("No active project");
331
+
332
+ const kind = getTimelineAssetKind(assetPath);
333
+ if (!kind) {
334
+ showToast("Only image, video, and audio assets can be dropped onto the timeline.");
335
+ return;
336
+ }
337
+
338
+ const targetPath = activeCompPath || "index.html";
339
+ try {
340
+ const originalContent = await readFileContent(pid, targetPath);
341
+
342
+ const normalizedStart = Number(formatTimelineAttributeNumber(placement.start));
343
+ const duration =
344
+ Number.isFinite(durationOverride) && durationOverride != null && durationOverride > 0
345
+ ? durationOverride
346
+ : await resolveDroppedAssetDuration(pid, assetPath, kind);
347
+ const normalizedDuration = Number(formatTimelineAttributeNumber(duration));
348
+ const newId = buildTimelineAssetId(assetPath, collectHtmlIds(originalContent));
349
+ const resolvedAssetSrc = resolveTimelineAssetSrc(targetPath, assetPath);
350
+
351
+ const resolvedTargetPath = targetPath || "index.html";
352
+ const relevantElements = timelineElements.filter(
353
+ (te) => (te.sourceFile || activeCompPath || "index.html") === resolvedTargetPath,
354
+ );
355
+ const trackZIndices = buildTrackZIndexMap([
356
+ ...relevantElements.map((te) => te.track),
357
+ placement.track,
358
+ ]);
359
+
360
+ let patchedContent = originalContent;
361
+ for (const te of relevantElements) {
362
+ const elementTarget = buildPatchTarget(te);
363
+ if (!elementTarget) continue;
364
+ const nextZIndex = trackZIndices.get(te.track);
365
+ if (nextZIndex == null) continue;
366
+ patchedContent = applyPatchByTarget(patchedContent, elementTarget, {
367
+ type: "inline-style",
368
+ property: "z-index",
369
+ value: String(nextZIndex),
370
+ });
371
+ }
372
+
373
+ patchedContent = insertTimelineAssetIntoSource(
374
+ patchedContent,
375
+ buildTimelineAssetInsertHtml({
376
+ id: newId,
377
+ assetPath: resolvedAssetSrc,
378
+ kind,
379
+ start: normalizedStart,
380
+ duration: normalizedDuration,
381
+ track: placement.track,
382
+ zIndex: trackZIndices.get(placement.track) ?? 1,
383
+ geometry: resolveTimelineAssetInitialGeometry(originalContent),
384
+ }),
385
+ );
386
+
387
+ domEditSaveTimestampRef.current = Date.now();
388
+ await saveProjectFilesWithHistory({
389
+ projectId: pid,
390
+ label: "Add timeline asset",
391
+ kind: "timeline",
392
+ files: { [targetPath]: patchedContent },
393
+ readFile: async () => originalContent,
394
+ writeFile: writeProjectFile,
395
+ recordEdit,
396
+ });
397
+
398
+ reloadPreview();
399
+ } catch (error) {
400
+ const message =
401
+ error instanceof Error ? error.message : "Failed to drop asset onto timeline";
402
+ showToast(message);
403
+ }
404
+ },
405
+ [
406
+ activeCompPath,
407
+ recordEdit,
408
+ showToast,
409
+ timelineElements,
410
+ writeProjectFile,
411
+ domEditSaveTimestampRef,
412
+ reloadPreview,
413
+ ],
414
+ );
415
+
416
+ const handleTimelineFileDrop = useCallback(
417
+ async (files: File[], placement?: Pick<TimelineElement, "start" | "track">) => {
418
+ const pid = projectIdRef.current;
419
+ if (!pid) return;
420
+ const uploaded = await uploadProjectFiles(files);
421
+ if (uploaded.length === 0) return;
422
+ const durations: number[] = [];
423
+ for (const assetPath of uploaded) {
424
+ const kind = getTimelineAssetKind(assetPath);
425
+ const duration = kind ? await resolveDroppedAssetDuration(pid, assetPath, kind) : 0;
426
+ durations.push(Number(formatTimelineAttributeNumber(duration)));
427
+ }
428
+ const placements = buildTimelineFileDropPlacements(
429
+ placement ?? { start: 0, track: 0 },
430
+ durations,
431
+ timelineElements
432
+ .filter(
433
+ (te) =>
434
+ (te.sourceFile || activeCompPath || "index.html") ===
435
+ (activeCompPath || "index.html"),
436
+ )
437
+ .map((te) => ({
438
+ start: te.start,
439
+ duration: te.duration,
440
+ track: te.track,
441
+ })),
442
+ );
443
+ for (const [index, assetPath] of uploaded.entries()) {
444
+ await handleTimelineAssetDrop(
445
+ assetPath,
446
+ placements[index] ?? placements[0],
447
+ durations[index],
448
+ );
449
+ }
450
+ },
451
+ [activeCompPath, handleTimelineAssetDrop, timelineElements, uploadProjectFiles],
452
+ );
453
+
454
+ const handleBlockedTimelineEdit = useCallback(
455
+ (_element: TimelineElement) => {
456
+ const now = Date.now();
457
+ if (now - lastBlockedTimelineToastAtRef.current < 1500) return;
458
+ lastBlockedTimelineToastAtRef.current = now;
459
+ showToast("This clip can't be moved or resized from the timeline yet.", "info");
460
+ },
461
+ [showToast],
462
+ );
463
+
464
+ return {
465
+ handleTimelineElementMove,
466
+ handleTimelineElementResize,
467
+ handleTimelineElementDelete,
468
+ handleTimelineAssetDrop,
469
+ handleTimelineFileDrop,
470
+ handleBlockedTimelineEdit,
471
+ };
472
+ }
@@ -36,6 +36,8 @@ function getShaderTransitionLoading(event: Event): boolean | null {
36
36
  return state.loading === true && state.ready !== true;
37
37
  }
38
38
 
39
+ const COMPOSITION_LOADING_OVERLAY_DELAY_MS = 400;
40
+
39
41
  export function shouldShowCompositionLoadingOverlay(compositionLoading: boolean): boolean {
40
42
  return compositionLoading;
41
43
  }
@@ -124,6 +126,20 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
124
126
  const [assetOverlayFading, setAssetOverlayFading] = useState(false);
125
127
  const [shaderTransitionLoading, setShaderTransitionLoading] = useState(false);
126
128
  const [compositionLoading, setCompositionLoading] = useState(true);
129
+ const [compositionOverlayDeferred, setCompositionOverlayDeferred] = useState(true);
130
+
131
+ // eslint-disable-next-line no-restricted-syntax
132
+ useEffect(() => {
133
+ if (!compositionLoading) {
134
+ setCompositionOverlayDeferred(true);
135
+ return;
136
+ }
137
+ const timer = setTimeout(
138
+ () => setCompositionOverlayDeferred(false),
139
+ COMPOSITION_LOADING_OVERLAY_DELAY_MS,
140
+ );
141
+ return () => clearTimeout(timer);
142
+ }, [compositionLoading]);
127
143
 
128
144
  useMountEffect(() => {
129
145
  const container = containerRef.current;
@@ -281,7 +297,9 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
281
297
  }, [assetsLoading]);
282
298
 
283
299
  const showCompositionOverlay =
284
- !suppressLoadingOverlay && shouldShowCompositionLoadingOverlay(compositionLoading);
300
+ !suppressLoadingOverlay &&
301
+ !compositionOverlayDeferred &&
302
+ shouldShowCompositionLoadingOverlay(compositionLoading);
285
303
  const showAssetOverlay =
286
304
  assetOverlayVisible && !shaderTransitionLoading && !showCompositionOverlay;
287
305
 
@@ -12,8 +12,6 @@ import {
12
12
  shouldHandleTimelineDeleteKey,
13
13
  shouldAutoScrollTimeline,
14
14
  } from "./Timeline";
15
- import { TIMELINE_CLIP_CONTROL_Z_INDEX } from "./TimelineClip";
16
- import { COMPOSITION_THUMBNAIL_LABEL_Z_INDEX } from "./CompositionThumbnail";
17
15
  import { formatTime } from "../lib/time";
18
16
 
19
17
  describe("generateTicks", () => {
@@ -166,12 +164,6 @@ describe("shouldAutoScrollTimeline", () => {
166
164
  });
167
165
  });
168
166
 
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
-
175
167
  describe("getTimelineScrollLeftForZoomTransition", () => {
176
168
  it("resets horizontal scroll when switching from manual zoom back to fit", () => {
177
169
  expect(getTimelineScrollLeftForZoomTransition("manual", "fit", 480)).toBe(0);