@hyperframes/studio 0.7.39 → 0.7.40

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 (76) hide show
  1. package/dist/assets/{index-312a3Ceu.js → index-89kPtC4s.js} +1 -1
  2. package/dist/assets/{index-B9YvRJz1.js → index-B2Utv-2b.js} +195 -195
  3. package/dist/assets/index-BpM6cnfP.css +1 -0
  4. package/dist/assets/{index-CQHiecE7.js → index-CjX8Ljc8.js} +1 -1
  5. package/dist/{chunk-RCLGSZ7C.js → chunk-BA66NM4L.js} +1 -1
  6. package/dist/{chunk-RCLGSZ7C.js.map → chunk-BA66NM4L.js.map} +1 -1
  7. package/dist/{domEditingLayers-S6YOLVRG.js → domEditingLayers-H7LDFIJ7.js} +2 -2
  8. package/dist/index.d.ts +34 -29
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +2749 -2075
  11. package/dist/index.js.map +1 -1
  12. package/package.json +7 -7
  13. package/src/App.tsx +2 -1
  14. package/src/components/StudioPreviewArea.tsx +6 -0
  15. package/src/components/StudioRightPanel.tsx +3 -0
  16. package/src/components/editor/DomEditOverlay.tsx +33 -47
  17. package/src/components/editor/PropertyPanel.test.ts +25 -0
  18. package/src/components/editor/PropertyPanel.tsx +33 -7
  19. package/src/components/editor/domEditingElement.ts +8 -1
  20. package/src/components/editor/manualEditsDom.ts +1 -18
  21. package/src/components/editor/manualEditsStyleHelpers.ts +18 -0
  22. package/src/components/editor/offCanvasIndicatorGeometry.ts +74 -0
  23. package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +154 -0
  24. package/src/components/editor/offCanvasIndicatorRefresh.ts +99 -0
  25. package/src/components/editor/propertyPanelHelpers.ts +11 -0
  26. package/src/components/editor/propertyPanelTypes.ts +1 -0
  27. package/src/contexts/TimelineEditContext.tsx +1 -0
  28. package/src/hooks/gsapDragCommit.test.ts +56 -0
  29. package/src/hooks/gsapDragCommit.ts +41 -109
  30. package/src/hooks/gsapDragPositionCommit.ts +3 -0
  31. package/src/hooks/gsapDragStaticSetHelpers.ts +109 -0
  32. package/src/hooks/gsapRuntimeBridge.ts +23 -2
  33. package/src/hooks/gsapRuntimeKeyframes.test.ts +34 -0
  34. package/src/hooks/gsapRuntimeKeyframes.ts +13 -1
  35. package/src/hooks/gsapWholePropertyOffsetCommit.ts +1 -0
  36. package/src/hooks/timelineEditingHelpers.ts +17 -5
  37. package/src/hooks/timelineTrackVisibility.test.ts +202 -0
  38. package/src/hooks/timelineTrackVisibility.ts +371 -0
  39. package/src/hooks/useRenderClipContent.test.ts +68 -2
  40. package/src/hooks/useRenderClipContent.ts +5 -6
  41. package/src/hooks/useTimelineEditing.ts +35 -30
  42. package/src/hooks/useTimelineEditingTypes.ts +30 -0
  43. package/src/icons/SystemIcons.tsx +12 -4
  44. package/src/player/components/AudioWaveform.tsx +11 -8
  45. package/src/player/components/CompositionThumbnail.tsx +14 -11
  46. package/src/player/components/PlayheadIndicator.tsx +21 -11
  47. package/src/player/components/Timeline.test.ts +105 -5
  48. package/src/player/components/Timeline.tsx +37 -47
  49. package/src/player/components/TimelineCanvas.tsx +275 -256
  50. package/src/player/components/TimelineClip.test.tsx +105 -0
  51. package/src/player/components/TimelineClip.tsx +44 -57
  52. package/src/player/components/TimelineRuler.tsx +17 -23
  53. package/src/player/components/TimelineShortcutHint.tsx +26 -0
  54. package/src/player/components/TimelineTypes.ts +18 -0
  55. package/src/player/components/VideoThumbnail.tsx +16 -13
  56. package/src/player/components/timelineCallbacks.ts +1 -0
  57. package/src/player/components/timelineIcons.tsx +2 -44
  58. package/src/player/components/timelineLayout.ts +18 -22
  59. package/src/player/components/timelineMotionStyles.test.ts +101 -0
  60. package/src/player/components/timelineTheme.test.ts +21 -4
  61. package/src/player/components/timelineTheme.ts +15 -15
  62. package/src/player/components/useTimelineActiveClips.test.ts +93 -0
  63. package/src/player/components/useTimelineActiveClips.ts +125 -0
  64. package/src/player/components/useTimelinePlayhead.ts +6 -0
  65. package/src/player/hooks/useExpandedTimelineElements.test.ts +110 -3
  66. package/src/player/hooks/useExpandedTimelineElements.ts +106 -19
  67. package/src/player/lib/time.test.ts +1 -0
  68. package/src/player/lib/timelineDOM.test.ts +48 -1
  69. package/src/player/lib/timelineDOM.ts +7 -0
  70. package/src/player/lib/timelineIframeHelpers.ts +1 -0
  71. package/src/player/store/playerStore.ts +5 -1
  72. package/src/styles/studio.css +108 -0
  73. package/src/utils/sourcePatcher.test.ts +11 -0
  74. package/src/utils/sourcePatcher.ts +6 -4
  75. package/dist/assets/index-0P10SwC_.css +0 -1
  76. /package/dist/{domEditingLayers-S6YOLVRG.js.map → domEditingLayers-H7LDFIJ7.js.map} +0 -0
@@ -0,0 +1,371 @@
1
+ import { useCallback } from "react";
2
+ import { usePlayerStore, type TimelineElement } from "../player";
3
+ import { useExpandedTimelineElements } from "../player/hooks/useExpandedTimelineElements";
4
+ import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
5
+ import { readTagSnippetByTarget, type PatchOperation } from "../utils/sourcePatcher";
6
+ import {
7
+ applyPatchByTarget,
8
+ buildPatchTarget,
9
+ findTimelineElementInIframe,
10
+ readFileContent,
11
+ type RecordEditInput,
12
+ } from "./timelineEditingHelpers";
13
+
14
+ interface MutableRef<T> {
15
+ current: T;
16
+ }
17
+
18
+ interface ReadonlyRef<T> {
19
+ readonly current: T;
20
+ }
21
+
22
+ interface ToggleTimelineTrackHiddenInput {
23
+ projectId: string;
24
+ activeCompPath: string | null;
25
+ timelineElements: readonly TimelineElement[];
26
+ track: number;
27
+ hidden: boolean;
28
+ previewIframe: HTMLIFrameElement | null;
29
+ writeProjectFile: (path: string, content: string) => Promise<void>;
30
+ recordEdit: (input: RecordEditInput) => Promise<void>;
31
+ domEditSaveTimestampRef: MutableRef<number>;
32
+ pendingTimelineEditPathRef: MutableRef<Set<string>>;
33
+ }
34
+
35
+ interface ToggleTimelineElementHiddenInput extends Omit<ToggleTimelineTrackHiddenInput, "track"> {
36
+ elementKey: string;
37
+ }
38
+
39
+ interface SetElementsHiddenInput {
40
+ projectId: string;
41
+ activeCompPath: string | null;
42
+ elements: readonly TimelineElement[];
43
+ hidden: boolean;
44
+ label: string;
45
+ previewIframe: HTMLIFrameElement | null;
46
+ writeProjectFile: (path: string, content: string) => Promise<void>;
47
+ recordEdit: (input: RecordEditInput) => Promise<void>;
48
+ domEditSaveTimestampRef: MutableRef<number>;
49
+ pendingTimelineEditPathRef: MutableRef<Set<string>>;
50
+ }
51
+
52
+ interface UseTimelineTrackVisibilityEditingInput extends Omit<
53
+ ToggleTimelineTrackHiddenInput,
54
+ "projectId" | "track" | "hidden" | "previewIframe"
55
+ > {
56
+ projectIdRef: ReadonlyRef<string | null>;
57
+ previewIframeRef: ReadonlyRef<HTMLIFrameElement | null>;
58
+ showToast: (message: string, tone?: "error" | "info") => void;
59
+ isRecordingRef?: ReadonlyRef<boolean>;
60
+ forceReloadSdkSession?: () => void;
61
+ }
62
+
63
+ interface UseTimelineElementVisibilityEditingInput extends Omit<
64
+ ToggleTimelineElementHiddenInput,
65
+ "projectId" | "elementKey" | "hidden" | "previewIframe"
66
+ > {
67
+ projectIdRef: ReadonlyRef<string | null>;
68
+ previewIframeRef: ReadonlyRef<HTMLIFrameElement | null>;
69
+ showToast: (message: string, tone?: "error" | "info") => void;
70
+ isRecordingRef?: ReadonlyRef<boolean>;
71
+ forceReloadSdkSession?: () => void;
72
+ }
73
+
74
+ function getTimelineElementTargetPath(
75
+ element: TimelineElement,
76
+ activeCompPath: string | null,
77
+ ): string {
78
+ return element.sourceFile || activeCompPath || "index.html";
79
+ }
80
+
81
+ function patchLiveHiddenState(
82
+ iframe: HTMLIFrameElement | null,
83
+ elements: readonly TimelineElement[],
84
+ hidden: boolean,
85
+ ): void {
86
+ for (const element of elements) {
87
+ const target = findTimelineElementInIframe(iframe, element);
88
+ if (!target) continue;
89
+ if (hidden) {
90
+ target.setAttribute("data-hidden", "");
91
+ } else {
92
+ target.removeAttribute("data-hidden");
93
+ }
94
+ }
95
+ }
96
+
97
+ function reseekPreviewRuntime(iframe: HTMLIFrameElement | null): void {
98
+ try {
99
+ const win: (Window & { __player?: { seek?: (time: number) => void } }) | null =
100
+ iframe?.contentWindow ?? null;
101
+ win?.__player?.seek?.(usePlayerStore.getState().currentTime);
102
+ } catch {}
103
+ }
104
+
105
+ function groupElementsByTargetPath(
106
+ elements: readonly TimelineElement[],
107
+ activeCompPath: string | null,
108
+ ): Map<string, TimelineElement[]> {
109
+ const byPath = new Map<string, TimelineElement[]>();
110
+ for (const element of elements) {
111
+ const targetPath = getTimelineElementTargetPath(element, activeCompPath);
112
+ const existing = byPath.get(targetPath);
113
+ if (existing) {
114
+ existing.push(element);
115
+ } else {
116
+ byPath.set(targetPath, [element]);
117
+ }
118
+ }
119
+ return byPath;
120
+ }
121
+
122
+ // fallow-ignore-next-line complexity
123
+ async function setElementsHidden({
124
+ projectId,
125
+ activeCompPath,
126
+ elements,
127
+ hidden,
128
+ label,
129
+ previewIframe,
130
+ writeProjectFile,
131
+ recordEdit,
132
+ domEditSaveTimestampRef,
133
+ pendingTimelineEditPathRef,
134
+ }: SetElementsHiddenInput): Promise<string[]> {
135
+ if (elements.length === 0) return [];
136
+
137
+ patchLiveHiddenState(previewIframe, elements, hidden);
138
+ reseekPreviewRuntime(previewIframe);
139
+
140
+ const hiddenOperation: PatchOperation = {
141
+ type: "attribute",
142
+ property: "hidden",
143
+ value: hidden ? "" : null,
144
+ };
145
+ const originalByPath = new Map<string, string>();
146
+ const files: Record<string, string> = {};
147
+
148
+ try {
149
+ for (const [targetPath, fileElements] of groupElementsByTargetPath(elements, activeCompPath)) {
150
+ let patchedContent = await readFileContent(projectId, targetPath);
151
+ originalByPath.set(targetPath, patchedContent);
152
+
153
+ for (const element of fileElements) {
154
+ const patchTarget = buildPatchTarget(element);
155
+ if (!patchTarget) {
156
+ throw new Error(`Timeline element ${element.id} is missing a patchable target`);
157
+ }
158
+ if (readTagSnippetByTarget(patchedContent, patchTarget) === undefined) {
159
+ throw new Error(`Unable to patch timeline element ${element.id} in ${targetPath}`);
160
+ }
161
+ patchedContent = applyPatchByTarget(patchedContent, patchTarget, hiddenOperation);
162
+ }
163
+
164
+ files[targetPath] = patchedContent;
165
+ pendingTimelineEditPathRef.current.add(targetPath);
166
+ }
167
+
168
+ domEditSaveTimestampRef.current = Date.now();
169
+ const changedPaths = await saveProjectFilesWithHistory({
170
+ projectId,
171
+ label,
172
+ kind: "timeline",
173
+ files,
174
+ readFile: async (path) => {
175
+ const original = originalByPath.get(path);
176
+ if (original !== undefined) return original;
177
+ return readFileContent(projectId, path);
178
+ },
179
+ writeFile: writeProjectFile,
180
+ recordEdit,
181
+ });
182
+ domEditSaveTimestampRef.current = Date.now();
183
+ for (const element of elements) {
184
+ usePlayerStore.getState().updateElement(element.key ?? element.id, { hidden });
185
+ }
186
+ return changedPaths;
187
+ } catch (error) {
188
+ // The optimistic live patch already ran; a patch-target/save failure here would
189
+ // otherwise leave the preview showing the wrong visibility until a reload. Revert
190
+ // the live DOM to the prior state so what's on screen matches what persisted.
191
+ patchLiveHiddenState(previewIframe, elements, !hidden);
192
+ reseekPreviewRuntime(previewIframe);
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ export async function toggleTimelineTrackHidden({
198
+ projectId,
199
+ activeCompPath,
200
+ timelineElements,
201
+ track,
202
+ hidden,
203
+ previewIframe,
204
+ writeProjectFile,
205
+ recordEdit,
206
+ domEditSaveTimestampRef,
207
+ pendingTimelineEditPathRef,
208
+ }: ToggleTimelineTrackHiddenInput): Promise<string[]> {
209
+ return setElementsHidden({
210
+ projectId,
211
+ activeCompPath,
212
+ elements: timelineElements.filter((element) => element.track === track),
213
+ hidden,
214
+ label: hidden ? `Hide track ${track}` : `Show track ${track}`,
215
+ previewIframe,
216
+ writeProjectFile,
217
+ recordEdit,
218
+ domEditSaveTimestampRef,
219
+ pendingTimelineEditPathRef,
220
+ });
221
+ }
222
+
223
+ export async function toggleTimelineElementHidden({
224
+ projectId,
225
+ activeCompPath,
226
+ timelineElements,
227
+ elementKey,
228
+ hidden,
229
+ previewIframe,
230
+ writeProjectFile,
231
+ recordEdit,
232
+ domEditSaveTimestampRef,
233
+ pendingTimelineEditPathRef,
234
+ }: ToggleTimelineElementHiddenInput): Promise<string[]> {
235
+ const element = timelineElements.find((item) => (item.key ?? item.id) === elementKey);
236
+ return setElementsHidden({
237
+ projectId,
238
+ activeCompPath,
239
+ elements: element ? [element] : [],
240
+ hidden,
241
+ label: hidden ? "Hide element" : "Show element",
242
+ previewIframe,
243
+ writeProjectFile,
244
+ recordEdit,
245
+ domEditSaveTimestampRef,
246
+ pendingTimelineEditPathRef,
247
+ });
248
+ }
249
+
250
+ export function useTimelineTrackVisibilityEditing({
251
+ projectIdRef,
252
+ activeCompPath,
253
+ showToast,
254
+ writeProjectFile,
255
+ recordEdit,
256
+ domEditSaveTimestampRef,
257
+ previewIframeRef,
258
+ pendingTimelineEditPathRef,
259
+ isRecordingRef,
260
+ forceReloadSdkSession,
261
+ }: UseTimelineTrackVisibilityEditingInput): (track: number, hidden: boolean) => Promise<void> {
262
+ // Resolve the eye toggle against the EXPANDED rows the canvas actually renders:
263
+ // virtual sub-comp children carry their own (display.track + idx) track numbers,
264
+ // so filtering the raw store list by a virtual track number would hide the wrong
265
+ // outer-scene sibling sharing that index.
266
+ const expandedElements = useExpandedTimelineElements();
267
+ return useCallback(
268
+ async (track: number, hidden: boolean) => {
269
+ if (isRecordingRef?.current) {
270
+ showToast("Cannot edit timeline while recording", "error");
271
+ return;
272
+ }
273
+ const pid = projectIdRef.current;
274
+ if (!pid) return;
275
+ try {
276
+ await toggleTimelineTrackHidden({
277
+ projectId: pid,
278
+ activeCompPath,
279
+ timelineElements: expandedElements,
280
+ track,
281
+ hidden,
282
+ previewIframe: previewIframeRef.current,
283
+ writeProjectFile,
284
+ recordEdit,
285
+ domEditSaveTimestampRef,
286
+ pendingTimelineEditPathRef,
287
+ });
288
+ forceReloadSdkSession?.();
289
+ } catch (error) {
290
+ console.error("[Timeline] Failed to toggle track visibility", error);
291
+ const message =
292
+ error instanceof Error ? error.message : "Failed to toggle track visibility";
293
+ showToast(message);
294
+ }
295
+ },
296
+ [
297
+ activeCompPath,
298
+ expandedElements,
299
+ previewIframeRef,
300
+ writeProjectFile,
301
+ recordEdit,
302
+ domEditSaveTimestampRef,
303
+ pendingTimelineEditPathRef,
304
+ isRecordingRef,
305
+ showToast,
306
+ forceReloadSdkSession,
307
+ projectIdRef,
308
+ ],
309
+ );
310
+ }
311
+
312
+ export function useTimelineElementVisibilityEditing({
313
+ projectIdRef,
314
+ activeCompPath,
315
+ timelineElements,
316
+ showToast,
317
+ writeProjectFile,
318
+ recordEdit,
319
+ domEditSaveTimestampRef,
320
+ previewIframeRef,
321
+ pendingTimelineEditPathRef,
322
+ isRecordingRef,
323
+ forceReloadSdkSession,
324
+ }: UseTimelineElementVisibilityEditingInput): (
325
+ elementKey: string,
326
+ hidden: boolean,
327
+ ) => Promise<void> {
328
+ return useCallback(
329
+ async (elementKey: string, hidden: boolean) => {
330
+ if (isRecordingRef?.current) {
331
+ showToast("Cannot edit timeline while recording", "error");
332
+ return;
333
+ }
334
+ const pid = projectIdRef.current;
335
+ if (!pid) return;
336
+ try {
337
+ await toggleTimelineElementHidden({
338
+ projectId: pid,
339
+ activeCompPath,
340
+ timelineElements,
341
+ elementKey,
342
+ hidden,
343
+ previewIframe: previewIframeRef.current,
344
+ writeProjectFile,
345
+ recordEdit,
346
+ domEditSaveTimestampRef,
347
+ pendingTimelineEditPathRef,
348
+ });
349
+ forceReloadSdkSession?.();
350
+ } catch (error) {
351
+ console.error("[Timeline] Failed to toggle element visibility", error);
352
+ const message =
353
+ error instanceof Error ? error.message : "Failed to toggle element visibility";
354
+ showToast(message);
355
+ }
356
+ },
357
+ [
358
+ activeCompPath,
359
+ timelineElements,
360
+ previewIframeRef,
361
+ writeProjectFile,
362
+ recordEdit,
363
+ domEditSaveTimestampRef,
364
+ pendingTimelineEditPathRef,
365
+ isRecordingRef,
366
+ showToast,
367
+ forceReloadSdkSession,
368
+ projectIdRef,
369
+ ],
370
+ );
371
+ }
@@ -3,6 +3,7 @@
3
3
  import React, { act, isValidElement, type ReactNode } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
5
  import { afterEach, describe, expect, it } from "vitest";
6
+ import { CompositionThumbnail, VideoThumbnail } from "../player";
6
7
  import { AudioWaveform } from "../player/components/AudioWaveform";
7
8
  import type { TimelineElement } from "../player/store/playerStore";
8
9
  import { normalizeCompositionSrc } from "./useRenderClipContent";
@@ -63,7 +64,10 @@ describe("normalizeCompositionSrc", () => {
63
64
  });
64
65
 
65
66
  describe("useRenderClipContent", () => {
66
- function renderClipContent(el: TimelineElement): ReactNode {
67
+ function renderClipContent(
68
+ el: TimelineElement,
69
+ activePreviewUrl: string | null = "/api/projects/my-project/preview",
70
+ ): ReactNode {
67
71
  const host = document.createElement("div");
68
72
  document.body.append(host);
69
73
  const root = createRoot(host);
@@ -73,7 +77,7 @@ describe("useRenderClipContent", () => {
73
77
  const render = useRenderClipContent({
74
78
  projectIdRef: { current: "my-project" },
75
79
  compIdToSrc: new Map(),
76
- activePreviewUrl: "/api/projects/my-project/preview",
80
+ activePreviewUrl,
77
81
  effectiveTimelineDuration: 12,
78
82
  });
79
83
  content = render(el, { clip: "#222", label: "#fff" });
@@ -100,4 +104,66 @@ describe("useRenderClipContent", () => {
100
104
  expect(isValidElement(content)).toBe(true);
101
105
  if (isValidElement(content)) expect(content.type).toBe(AudioWaveform);
102
106
  });
107
+
108
+ it("passes empty labels to thumbnail content so TimelineClip owns clip names", () => {
109
+ const cases: Array<{ content: ReactNode; type: unknown }> = [
110
+ {
111
+ content: renderClipContent({
112
+ id: "voiceover",
113
+ tag: "audio",
114
+ start: 1,
115
+ duration: 4,
116
+ track: 1,
117
+ src: "assets/voiceover.mp3",
118
+ }),
119
+ type: AudioWaveform,
120
+ },
121
+ {
122
+ content: renderClipContent({
123
+ id: "nested",
124
+ tag: "div",
125
+ start: 0,
126
+ duration: 4,
127
+ track: 0,
128
+ compositionSrc: "compositions/nested.html",
129
+ }),
130
+ type: CompositionThumbnail,
131
+ },
132
+ {
133
+ content: renderClipContent(
134
+ {
135
+ id: "clip-video",
136
+ tag: "video",
137
+ start: 0,
138
+ duration: 4,
139
+ track: 0,
140
+ src: "assets/clip.mp4",
141
+ },
142
+ null,
143
+ ),
144
+ type: VideoThumbnail,
145
+ },
146
+ {
147
+ content: renderClipContent(
148
+ {
149
+ id: "headline",
150
+ tag: "div",
151
+ start: 1,
152
+ duration: 4,
153
+ track: 0,
154
+ },
155
+ null,
156
+ ),
157
+ type: CompositionThumbnail,
158
+ },
159
+ ];
160
+
161
+ for (const item of cases) {
162
+ expect(isValidElement<{ label: string }>(item.content)).toBe(true);
163
+ if (isValidElement<{ label: string }>(item.content)) {
164
+ expect(item.content.type).toBe(item.type);
165
+ expect(item.content.props.label).toBe("");
166
+ }
167
+ }
168
+ });
103
169
  });
@@ -3,7 +3,6 @@ import { createElement } from "react";
3
3
  import { CompositionThumbnail, VideoThumbnail } from "../player";
4
4
  import type { TimelineElement } from "../player";
5
5
  import { AudioWaveform } from "../player/components/AudioWaveform";
6
- import { getTimelineElementLabel } from "../utils/studioHelpers";
7
6
 
8
7
  export function normalizeCompositionSrc(
9
8
  compSrc: string,
@@ -58,7 +57,7 @@ function renderAudioClip(el: TimelineElement, pid: string, labelColor: string):
58
57
  return createElement(AudioWaveform, {
59
58
  audioUrl,
60
59
  waveformUrl,
61
- label: getTimelineElementLabel(el),
60
+ label: "",
62
61
  labelColor,
63
62
  trimStartFraction: start,
64
63
  trimEndFraction: end,
@@ -102,7 +101,7 @@ export function useRenderClipContent({
102
101
  if (compSrc) {
103
102
  return createElement(CompositionThumbnail, {
104
103
  previewUrl: `/api/projects/${pid}/preview/comp/${compSrc}`,
105
- label: getTimelineElementLabel(el),
104
+ label: "",
106
105
  labelColor: style.label,
107
106
 
108
107
  seekTime: 0,
@@ -122,7 +121,7 @@ export function useRenderClipContent({
122
121
  if (activePreviewUrl && el.duration > 0) {
123
122
  return createElement(CompositionThumbnail, {
124
123
  previewUrl: activePreviewUrl,
125
- label: getTimelineElementLabel(el),
124
+ label: "",
126
125
  labelColor: style.label,
127
126
 
128
127
  selector: el.selector,
@@ -144,7 +143,7 @@ export function useRenderClipContent({
144
143
  : `/api/projects/${pid}/preview/${el.src}`;
145
144
  return createElement(VideoThumbnail, {
146
145
  videoSrc: mediaSrc,
147
- label: getTimelineElementLabel(el),
146
+ label: "",
148
147
  labelColor: style.label,
149
148
  duration: el.duration,
150
149
  });
@@ -153,7 +152,7 @@ export function useRenderClipContent({
153
152
  if (htmlPreviewEligible) {
154
153
  return createElement(CompositionThumbnail, {
155
154
  previewUrl: `/api/projects/${pid}/preview`,
156
- label: getTimelineElementLabel(el),
155
+ label: "",
157
156
  labelColor: style.label,
158
157
 
159
158
  selector: el.selector,
@@ -20,7 +20,6 @@ import {
20
20
  collectHtmlIds,
21
21
  resolveDroppedAssetDuration,
22
22
  } from "../utils/studioHelpers";
23
- import type { EditHistoryKind } from "../utils/editHistory";
24
23
  import {
25
24
  buildPatchTarget,
26
25
  patchIframeDomTiming,
@@ -33,36 +32,12 @@ import {
33
32
  scaleGsapPositions,
34
33
  } from "./timelineEditingHelpers";
35
34
  import type { PersistTimelineEditInput } from "./timelineEditingHelpers";
35
+ import {
36
+ useTimelineElementVisibilityEditing,
37
+ useTimelineTrackVisibilityEditing,
38
+ } from "./timelineTrackVisibility";
36
39
  import { sdkTimingPersist } from "../utils/sdkCutover";
37
- import type { Composition } from "@hyperframes/sdk";
38
-
39
- // ── Types ──
40
-
41
- interface RecordEditInput {
42
- label: string;
43
- kind: EditHistoryKind;
44
- coalesceKey?: string;
45
- files: Record<string, { before: string; after: string }>;
46
- }
47
-
48
- interface UseTimelineEditingOptions {
49
- projectId: string | null;
50
- activeCompPath: string | null;
51
- timelineElements: TimelineElement[];
52
- showToast: (message: string, tone?: "error" | "info") => void;
53
- writeProjectFile: (path: string, content: string) => Promise<void>;
54
- recordEdit: (input: RecordEditInput) => Promise<void>;
55
- domEditSaveTimestampRef: React.MutableRefObject<number>;
56
- reloadPreview: () => void;
57
- previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
58
- pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
59
- uploadProjectFiles: (files: Iterable<File>, dir?: string) => Promise<string[]>;
60
- isRecordingRef?: React.RefObject<boolean>;
61
- /** Stage 7 §3.2: SDK session for routing timing ops through setTiming. */
62
- sdkSession?: Composition | null;
63
- /** Resync the SDK session after a server-authoritative timeline write. */
64
- forceReloadSdkSession?: () => void;
65
- }
40
+ import type { UseTimelineEditingOptions } from "./useTimelineEditingTypes";
66
41
 
67
42
  // ── Hook ──
68
43
 
@@ -323,6 +298,34 @@ export function useTimelineEditing({
323
298
  ],
324
299
  );
325
300
 
301
+ const handleToggleTrackHidden = useTimelineTrackVisibilityEditing({
302
+ projectIdRef,
303
+ activeCompPath,
304
+ timelineElements,
305
+ showToast,
306
+ writeProjectFile,
307
+ recordEdit,
308
+ domEditSaveTimestampRef,
309
+ previewIframeRef,
310
+ pendingTimelineEditPathRef,
311
+ isRecordingRef,
312
+ forceReloadSdkSession,
313
+ });
314
+
315
+ const handleToggleElementHidden = useTimelineElementVisibilityEditing({
316
+ projectIdRef,
317
+ activeCompPath,
318
+ timelineElements,
319
+ showToast,
320
+ writeProjectFile,
321
+ recordEdit,
322
+ domEditSaveTimestampRef,
323
+ previewIframeRef,
324
+ pendingTimelineEditPathRef,
325
+ isRecordingRef,
326
+ forceReloadSdkSession,
327
+ });
328
+
326
329
  // fallow-ignore-next-line complexity
327
330
  const handleTimelineElementDelete = useCallback(
328
331
  // fallow-ignore-next-line complexity
@@ -562,6 +565,8 @@ export function useTimelineEditing({
562
565
  return {
563
566
  handleTimelineElementMove,
564
567
  handleTimelineElementResize,
568
+ handleToggleTrackHidden,
569
+ handleToggleElementHidden,
565
570
  handleTimelineElementDelete,
566
571
  handleTimelineElementSplit: handleRazorSplit,
567
572
  handleRazorSplit,
@@ -0,0 +1,30 @@
1
+ import type { MutableRefObject, RefObject } from "react";
2
+ import type { Composition } from "@hyperframes/sdk";
3
+ import type { TimelineElement } from "../player";
4
+ import type { EditHistoryKind } from "../utils/editHistory";
5
+
6
+ interface RecordEditInput {
7
+ label: string;
8
+ kind: EditHistoryKind;
9
+ coalesceKey?: string;
10
+ files: Record<string, { before: string; after: string }>;
11
+ }
12
+
13
+ export interface UseTimelineEditingOptions {
14
+ projectId: string | null;
15
+ activeCompPath: string | null;
16
+ timelineElements: TimelineElement[];
17
+ showToast: (message: string, tone?: "error" | "info") => void;
18
+ writeProjectFile: (path: string, content: string) => Promise<void>;
19
+ recordEdit: (input: RecordEditInput) => Promise<void>;
20
+ domEditSaveTimestampRef: MutableRefObject<number>;
21
+ reloadPreview: () => void;
22
+ previewIframeRef: RefObject<HTMLIFrameElement | null>;
23
+ pendingTimelineEditPathRef: MutableRefObject<Set<string>>;
24
+ uploadProjectFiles: (files: Iterable<File>, dir?: string) => Promise<string[]>;
25
+ isRecordingRef?: RefObject<boolean>;
26
+ /** Stage 7 §3.2: SDK session for routing timing ops through setTiming. */
27
+ sdkSession?: Composition | null;
28
+ /** Resync the SDK session after a server-authoritative timeline write. */
29
+ forceReloadSdkSession?: () => void;
30
+ }
@@ -4,7 +4,6 @@ import {
4
4
  Eye as PhEye,
5
5
  FilmStrip,
6
6
  Stack,
7
- ChatCenteredText,
8
7
  ArrowsOutCardinal,
9
8
  MusicNote,
10
9
  Palette as PhPalette,
@@ -15,7 +14,6 @@ import {
15
14
  TextT,
16
15
  X as PhX,
17
16
  Lightning,
18
- CaretDown,
19
17
  CaretRight,
20
18
  ClipboardText,
21
19
  ArrowCounterClockwise,
@@ -41,7 +39,6 @@ export const Clock = makeIcon(PhClock);
41
39
  export const Eye = makeIcon(PhEye);
42
40
  export const Film = makeIcon(FilmStrip);
43
41
  export const Layers = makeIcon(Stack);
44
- export const MessageSquare = makeIcon(ChatCenteredText);
45
42
  export const Move = makeIcon(ArrowsOutCardinal);
46
43
  export const Music = makeIcon(MusicNote);
47
44
  export const Palette = makeIcon(PhPalette);
@@ -53,7 +50,18 @@ export const Type = makeIcon(TextT);
53
50
  export const X = makeIcon(PhX);
54
51
  export const Zap = makeIcon(Lightning);
55
52
  // Extra icons used in this project (not in lucide's default mapping above)
56
- export const ChevronDown = makeIcon(CaretDown);
53
+ export const ChevronDown = ({ title, style, ...props }: IconProps) => {
54
+ const transform = style?.transform ? `${style.transform} rotate(90deg)` : "rotate(90deg)";
55
+ return (
56
+ <CaretRight
57
+ alt={title}
58
+ aria-label={title}
59
+ aria-hidden={title ? undefined : true}
60
+ style={{ ...style, transform }}
61
+ {...props}
62
+ />
63
+ );
64
+ };
57
65
  export const ChevronRight = makeIcon(CaretRight);
58
66
  export const ClipboardList = makeIcon(ClipboardText);
59
67
  export const RotateCcw = makeIcon(ArrowCounterClockwise);