@hyperframes/studio 0.7.37 → 0.7.38

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 (75) hide show
  1. package/dist/assets/hyperframes-player-BBrKzTGd.js +459 -0
  2. package/dist/assets/index-BLRTwY5l.js +396 -0
  3. package/dist/assets/{index-BXYJbIax.js → index-ClUipc8i.js} +1 -1
  4. package/dist/assets/index-DJaiR8T2.css +1 -0
  5. package/dist/assets/{index-Ct-X2wlf.js → index-Ykq7ihge.js} +1 -1
  6. package/dist/index.d.ts +35 -10
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +5146 -3321
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +19 -18
  12. package/src/components/AskAgentModal.tsx +23 -6
  13. package/src/components/LintModal.tsx +40 -10
  14. package/src/components/MediaPreview.tsx +36 -1
  15. package/src/components/SaveQueuePausedBanner.tsx +7 -6
  16. package/src/components/StudioErrorBoundary.tsx +19 -31
  17. package/src/components/StudioFeedbackBar.tsx +11 -2
  18. package/src/components/StudioGlobalDragOverlay.tsx +1 -1
  19. package/src/components/StudioHeader.tsx +184 -106
  20. package/src/components/StudioLeftSidebar.tsx +24 -3
  21. package/src/components/StudioOverlays.tsx +34 -8
  22. package/src/components/StudioRightPanel.tsx +154 -9
  23. package/src/components/StudioSplash.tsx +6 -3
  24. package/src/components/StudioToast.tsx +12 -11
  25. package/src/components/TimelineToolbar.test.tsx +9 -3
  26. package/src/components/TimelineToolbar.tsx +63 -29
  27. package/src/components/editor/PropertyPanel.tsx +18 -17
  28. package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
  29. package/src/components/editor/colorGradingScopePatch.ts +57 -0
  30. package/src/components/editor/manualEditingAvailability.test.ts +0 -12
  31. package/src/components/editor/manualEditingAvailability.ts +0 -6
  32. package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
  33. package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
  34. package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
  35. package/src/components/editor/propertyPanelHelpers.ts +22 -91
  36. package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
  37. package/src/components/editor/propertyPanelTypes.ts +111 -0
  38. package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
  39. package/src/components/nle/NLELayout.tsx +54 -51
  40. package/src/components/nle/NLEPreview.tsx +41 -5
  41. package/src/components/nle/TimelineResizeDivider.tsx +97 -0
  42. package/src/components/nle/usePreviewBlockDrop.ts +31 -7
  43. package/src/components/renders/RenderQueue.tsx +125 -24
  44. package/src/components/renders/RenderQueueItem.tsx +132 -64
  45. package/src/components/renders/useRenderQueue.ts +136 -24
  46. package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
  47. package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
  48. package/src/components/storyboard/StoryboardView.tsx +6 -0
  49. package/src/components/studioColorGradingScope.ts +124 -0
  50. package/src/components/studioMediaJobs.ts +123 -0
  51. package/src/components/ui/Button.tsx +20 -11
  52. package/src/components/ui/HyperframesLoader.tsx +9 -2
  53. package/src/components/ui/SearchInput.tsx +53 -0
  54. package/src/components/ui/Tooltip.tsx +52 -6
  55. package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
  56. package/src/components/ui/useDialogBehavior.ts +83 -0
  57. package/src/contexts/PanelLayoutContext.tsx +3 -0
  58. package/src/contexts/StudioContext.tsx +7 -0
  59. package/src/hooks/useCompositionContentLoader.ts +40 -0
  60. package/src/hooks/useEditorSave.ts +14 -0
  61. package/src/hooks/useFileManager.ts +39 -25
  62. package/src/hooks/useFrameCapture.ts +12 -1
  63. package/src/hooks/usePanelLayout.ts +1 -0
  64. package/src/hooks/usePreviewPersistence.ts +24 -15
  65. package/src/hooks/useStudioContextValue.ts +5 -0
  66. package/src/hooks/useToast.ts +66 -13
  67. package/src/styles/studio.css +61 -0
  68. package/src/utils/sdkResolverShadow.test.ts +53 -3
  69. package/src/utils/sdkResolverShadow.ts +41 -1
  70. package/src/utils/studioPendingEdits.test.ts +43 -0
  71. package/src/utils/studioPendingEdits.ts +45 -0
  72. package/src/utils/studioUiPreferences.ts +4 -0
  73. package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
  74. package/dist/assets/index-DmkOvZns.css +0 -1
  75. package/dist/assets/index-SYSbQyK4.js +0 -396
@@ -1,15 +1,19 @@
1
- import { useState } from "react";
2
- import { Check, ClipboardList, Film, Music } from "../../icons/SystemIcons";
1
+ import { useEffect, useState } from "react";
2
+ import { Check, ClipboardList, Film, Music, Scissors } from "../../icons/SystemIcons";
3
3
  import type { DomEditSelection } from "./domEditing";
4
4
  import {
5
+ type BackgroundRemovalProgress,
6
+ type BackgroundRemovalResult,
5
7
  formatNumericValue,
6
8
  formatTimingValue,
7
9
  LABEL,
8
10
  parseNumericValue,
9
11
  RESPONSIVE_GRID,
12
+ stripQueryAndHash,
10
13
  } from "./propertyPanelHelpers";
11
14
  import { Section, SegmentedControl, SelectField, SliderControl } from "./propertyPanelPrimitives";
12
15
 
16
+ // fallow-ignore-next-line complexity
13
17
  export function MediaSection({
14
18
  projectDir,
15
19
  element,
@@ -17,6 +21,7 @@ export function MediaSection({
17
21
  onSetStyle,
18
22
  onSetAttribute,
19
23
  onSetHtmlAttribute,
24
+ onRemoveBackground,
20
25
  }: {
21
26
  projectDir: string | null;
22
27
  element: DomEditSelection;
@@ -24,8 +29,19 @@ export function MediaSection({
24
29
  onSetStyle: (prop: string, value: string) => void | Promise<void>;
25
30
  onSetAttribute: (attr: string, value: string) => void | Promise<void>;
26
31
  onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
32
+ onRemoveBackground?: (
33
+ inputPath: string,
34
+ options: {
35
+ createBackgroundPlate?: boolean;
36
+ quality?: "fast" | "balanced" | "best";
37
+ onProgress?: (progress: BackgroundRemovalProgress) => void;
38
+ },
39
+ ) => Promise<BackgroundRemovalResult>;
27
40
  }) {
28
41
  const isVideo = element.tagName === "video";
42
+ const isAudio = element.tagName === "audio";
43
+ const isImage = element.tagName === "img";
44
+ const isVisualMedia = isVideo || isImage;
29
45
  const el = element.element;
30
46
 
31
47
  const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
@@ -53,15 +69,64 @@ export function MediaSection({
53
69
 
54
70
  const srcAttr = el.getAttribute("src") ?? "";
55
71
  const [copied, setCopied] = useState(false);
72
+ const [removeBusy, setRemoveBusy] = useState(false);
73
+ const [removeProgress, setRemoveProgress] = useState<BackgroundRemovalProgress | null>(null);
74
+ const [createPlate, setCreatePlate] = useState(false);
75
+ const [quality, setQuality] = useState<"fast" | "balanced" | "best">("balanced");
56
76
 
57
77
  const absoluteSrc =
58
78
  projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr;
79
+ const projectSrc =
80
+ srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr)
81
+ ? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr)
82
+ : "";
83
+ const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc);
84
+ const panelTitle = isImage ? "Image" : isVideo ? "Video" : "Audio";
85
+
86
+ useEffect(() => {
87
+ setRemoveProgress(null);
88
+ setCreatePlate(false);
89
+ }, [srcAttr]);
90
+
91
+ const applyCutoutResult = async (result: BackgroundRemovalResult) => {
92
+ await onSetHtmlAttribute("src", result.outputPath);
93
+ if (isVideo) {
94
+ await onSetAttribute("has-audio", "");
95
+ await onSetHtmlAttribute("muted", "true");
96
+ }
97
+ };
98
+
99
+ const runBackgroundRemoval = async () => {
100
+ if (!onRemoveBackground || !projectSrc || removeBusy) return;
101
+ setRemoveBusy(true);
102
+ setRemoveProgress({ status: "processing", progress: 0, stage: "Preparing" });
103
+ try {
104
+ const result = await onRemoveBackground(projectSrc, {
105
+ createBackgroundPlate: isVideo && createPlate,
106
+ quality,
107
+ onProgress: setRemoveProgress,
108
+ });
109
+ await applyCutoutResult(result);
110
+ setRemoveProgress({
111
+ status: "complete",
112
+ progress: 100,
113
+ stage: "Applied cutout",
114
+ ...result,
115
+ });
116
+ } catch (error) {
117
+ setRemoveProgress({
118
+ status: "failed",
119
+ progress: 0,
120
+ stage: "Failed",
121
+ error: error instanceof Error ? error.message : String(error),
122
+ });
123
+ } finally {
124
+ setRemoveBusy(false);
125
+ }
126
+ };
59
127
 
60
128
  return (
61
- <Section
62
- title={isVideo ? "Video" : "Audio"}
63
- icon={isVideo ? <Film size={15} /> : <Music size={15} />}
64
- >
129
+ <Section title={panelTitle} icon={isAudio ? <Music size={15} /> : <Film size={15} />}>
65
130
  <div className="space-y-4">
66
131
  {srcAttr && (
67
132
  <div className="min-w-0">
@@ -90,103 +155,192 @@ export function MediaSection({
90
155
  </div>
91
156
  )}
92
157
 
93
- <div className="grid min-w-0 gap-1.5">
94
- <span className={LABEL}>Volume</span>
95
- <SliderControl
96
- value={volumePercent}
97
- min={0}
98
- max={100}
99
- step={1}
100
- displayValue={`${volumePercent}%`}
101
- formatDisplayValue={(next) => `${Math.round(next)}%`}
102
- onCommit={(next) => {
103
- void onSetAttribute("volume", formatNumericValue(next / 100));
104
- }}
105
- />
106
- </div>
107
-
108
- <div className="grid min-w-0 gap-1.5">
109
- <span className={LABEL}>Playback rate</span>
110
- <SliderControl
111
- value={playbackRate * 100}
112
- min={25}
113
- max={300}
114
- step={5}
115
- displayValue={`${formatNumericValue(playbackRate)}x`}
116
- formatDisplayValue={(next) => `${formatNumericValue(next / 100)}x`}
117
- onCommit={(next) => {
118
- void onSetAttribute("playback-rate", formatNumericValue(next / 100));
119
- }}
120
- />
121
- </div>
122
-
123
- <div className="grid min-w-0 gap-1.5">
124
- <span className={LABEL}>Media start</span>
125
- <SliderControl
126
- value={Math.round(mediaStart * 100)}
127
- min={0}
128
- max={mediaStartMax * 100}
129
- step={10}
130
- displayValue={formatTimingValue(mediaStart)}
131
- formatDisplayValue={(next) => formatTimingValue(next / 100)}
132
- onCommit={(next) => {
133
- void onSetAttribute("media-start", (next / 100).toFixed(2));
134
- }}
135
- />
136
- </div>
137
-
138
- <div className={RESPONSIVE_GRID}>
139
- <div className="grid min-w-0 gap-1.5">
140
- <span className={LABEL}>Loop</span>
141
- <SegmentedControl
142
- value={hasLoop ? "on" : "off"}
143
- onChange={(next) => {
144
- void onSetHtmlAttribute("loop", next === "on" ? "true" : null);
145
- }}
146
- options={[
147
- { label: "On", value: "on" },
148
- { label: "Off", value: "off" },
149
- ]}
150
- />
151
- </div>
152
- <div className="grid min-w-0 gap-1.5">
153
- <span className={LABEL}>Muted</span>
154
- <SegmentedControl
155
- value={hasMuted ? "on" : "off"}
156
- onChange={(next) => {
157
- void onSetHtmlAttribute("muted", next === "on" ? "true" : null);
158
- }}
159
- options={[
160
- { label: "On", value: "on" },
161
- { label: "Off", value: "off" },
162
- ]}
163
- />
164
- </div>
165
- </div>
166
-
167
- {isVideo && (
168
- <div className="grid min-w-0 gap-1.5">
169
- <span className={LABEL}>Has audio track</span>
170
- <SegmentedControl
171
- value={hasAudio ? "yes" : "no"}
172
- onChange={(next) => {
173
- if (next === "yes") {
174
- void onSetAttribute("has-audio", "true");
175
- void onSetHtmlAttribute("muted", null);
176
- } else {
177
- void onSetAttribute("has-audio", "");
178
- void onSetHtmlAttribute("muted", "true");
158
+ {isVisualMedia && (
159
+ <div className="grid min-w-0 max-w-full gap-2 overflow-hidden rounded-md bg-panel-input/30 p-2">
160
+ <div className="flex min-w-0 items-center justify-between gap-2">
161
+ <div className="min-w-0">
162
+ <div className={LABEL}>Cutout</div>
163
+ <div className="mt-0.5 truncate text-[10px] text-panel-text-4">
164
+ Create transparent {isVideo ? "WebM video" : "PNG image"}
165
+ </div>
166
+ </div>
167
+ <button
168
+ type="button"
169
+ disabled={!canRemoveBackground || removeBusy}
170
+ onClick={(event) => {
171
+ event.stopPropagation();
172
+ void runBackgroundRemoval();
173
+ }}
174
+ className="flex h-8 flex-shrink-0 items-center gap-1.5 rounded-md bg-panel-input px-2.5 text-[11px] font-medium text-panel-text-2 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-50"
175
+ title={
176
+ canRemoveBackground
177
+ ? "Remove background and save a transparent asset"
178
+ : "Select a project-local image or video asset"
179
179
  }
180
- }}
181
- options={[
182
- { label: "Yes", value: "yes" },
183
- { label: "No", value: "no" },
184
- ]}
185
- />
180
+ >
181
+ <Scissors size={13} />
182
+ <span>{removeBusy ? "Working" : "Remove BG"}</span>
183
+ </button>
184
+ </div>
185
+
186
+ <div className="grid grid-cols-2 gap-2">
187
+ <SelectField
188
+ label="Quality"
189
+ value={quality}
190
+ onChange={(next) => setQuality(next as typeof quality)}
191
+ options={["fast", "balanced", "best"]}
192
+ />
193
+ {isVideo ? (
194
+ <div className="grid min-w-0 gap-1.5">
195
+ <span className={LABEL}>BG plate</span>
196
+ <SegmentedControl
197
+ value={createPlate ? "on" : "off"}
198
+ onChange={(next) => setCreatePlate(next === "on")}
199
+ options={[
200
+ { label: "On", value: "on" },
201
+ { label: "Off", value: "off" },
202
+ ]}
203
+ />
204
+ <span className="text-[10px] leading-tight text-panel-text-4">
205
+ Optional hole-cut background copy.
206
+ </span>
207
+ </div>
208
+ ) : (
209
+ <div />
210
+ )}
211
+ </div>
212
+
213
+ {removeProgress && (
214
+ <div className="space-y-1">
215
+ <div className="flex min-w-0 items-center justify-between gap-2 text-[10px] text-panel-text-4">
216
+ <span className="min-w-0 flex-1 truncate">
217
+ {removeProgress.error ?? removeProgress.stage ?? "Processing"}
218
+ </span>
219
+ <span>{Math.round(removeProgress.progress)}%</span>
220
+ </div>
221
+ <div className="h-1 overflow-hidden rounded-full bg-panel-border">
222
+ <div
223
+ className={`h-full rounded-full ${
224
+ removeProgress.status === "failed" ? "bg-red-400" : "bg-studio-accent"
225
+ }`}
226
+ style={{ width: `${Math.max(0, Math.min(100, removeProgress.progress))}%` }}
227
+ />
228
+ </div>
229
+ </div>
230
+ )}
231
+
232
+ {removeProgress?.status === "complete" && removeProgress.outputPath && (
233
+ <div
234
+ className="truncate text-[10px] font-medium text-panel-text-3"
235
+ title={removeProgress.outputPath}
236
+ >
237
+ Applied {removeProgress.outputPath}
238
+ </div>
239
+ )}
186
240
  </div>
187
241
  )}
188
242
 
189
- {isVideo && (
243
+ {(isVideo || isAudio) && (
244
+ <>
245
+ <div className="grid min-w-0 gap-1.5">
246
+ <span className={LABEL}>Volume</span>
247
+ <SliderControl
248
+ value={volumePercent}
249
+ min={0}
250
+ max={100}
251
+ step={1}
252
+ displayValue={`${volumePercent}%`}
253
+ formatDisplayValue={(next) => `${Math.round(next)}%`}
254
+ onCommit={(next) => {
255
+ void onSetAttribute("volume", formatNumericValue(next / 100));
256
+ }}
257
+ />
258
+ </div>
259
+
260
+ <div className="grid min-w-0 gap-1.5">
261
+ <span className={LABEL}>Playback rate</span>
262
+ <SliderControl
263
+ value={playbackRate * 100}
264
+ min={25}
265
+ max={300}
266
+ step={5}
267
+ displayValue={`${formatNumericValue(playbackRate)}x`}
268
+ formatDisplayValue={(next) => `${formatNumericValue(next / 100)}x`}
269
+ onCommit={(next) => {
270
+ void onSetAttribute("playback-rate", formatNumericValue(next / 100));
271
+ }}
272
+ />
273
+ </div>
274
+
275
+ <div className="grid min-w-0 gap-1.5">
276
+ <span className={LABEL}>Media start</span>
277
+ <SliderControl
278
+ value={Math.round(mediaStart * 100)}
279
+ min={0}
280
+ max={mediaStartMax * 100}
281
+ step={10}
282
+ displayValue={formatTimingValue(mediaStart)}
283
+ formatDisplayValue={(next) => formatTimingValue(next / 100)}
284
+ onCommit={(next) => {
285
+ void onSetAttribute("media-start", (next / 100).toFixed(2));
286
+ }}
287
+ />
288
+ </div>
289
+
290
+ <div className={RESPONSIVE_GRID}>
291
+ <div className="grid min-w-0 gap-1.5">
292
+ <span className={LABEL}>Loop</span>
293
+ <SegmentedControl
294
+ value={hasLoop ? "on" : "off"}
295
+ onChange={(next) => {
296
+ void onSetHtmlAttribute("loop", next === "on" ? "true" : null);
297
+ }}
298
+ options={[
299
+ { label: "On", value: "on" },
300
+ { label: "Off", value: "off" },
301
+ ]}
302
+ />
303
+ </div>
304
+ <div className="grid min-w-0 gap-1.5">
305
+ <span className={LABEL}>Muted</span>
306
+ <SegmentedControl
307
+ value={hasMuted ? "on" : "off"}
308
+ onChange={(next) => {
309
+ void onSetHtmlAttribute("muted", next === "on" ? "true" : null);
310
+ }}
311
+ options={[
312
+ { label: "On", value: "on" },
313
+ { label: "Off", value: "off" },
314
+ ]}
315
+ />
316
+ </div>
317
+ </div>
318
+
319
+ {isVideo && (
320
+ <div className="grid min-w-0 gap-1.5">
321
+ <span className={LABEL}>Has audio track</span>
322
+ <SegmentedControl
323
+ value={hasAudio ? "yes" : "no"}
324
+ onChange={(next) => {
325
+ if (next === "yes") {
326
+ void onSetAttribute("has-audio", "true");
327
+ void onSetHtmlAttribute("muted", null);
328
+ } else {
329
+ void onSetAttribute("has-audio", "");
330
+ void onSetHtmlAttribute("muted", "true");
331
+ }
332
+ }}
333
+ options={[
334
+ { label: "Yes", value: "yes" },
335
+ { label: "No", value: "no" },
336
+ ]}
337
+ />
338
+ </div>
339
+ )}
340
+ </>
341
+ )}
342
+
343
+ {isVisualMedia && (
190
344
  <>
191
345
  <div className={RESPONSIVE_GRID}>
192
346
  <SelectField
@@ -0,0 +1,111 @@
1
+ import type { RefObject } from "react";
2
+ import type { ArcPathSegment, GsapAnimation } from "@hyperframes/parsers/gsap-parser";
3
+ import type { DomEditSelection } from "./domEditing";
4
+ import type { ImportedFontAsset } from "./fontAssets";
5
+
6
+ export interface BackgroundRemovalProgress {
7
+ status: "processing" | "complete" | "failed";
8
+ progress: number;
9
+ stage?: string;
10
+ outputPath?: string;
11
+ backgroundOutputPath?: string;
12
+ error?: string;
13
+ provider?: string;
14
+ }
15
+
16
+ export interface BackgroundRemovalResult {
17
+ outputPath: string;
18
+ backgroundOutputPath?: string;
19
+ provider?: string;
20
+ }
21
+
22
+ export interface PropertyPanelProps {
23
+ projectId: string;
24
+ projectDir: string | null;
25
+ assets: string[];
26
+ element: DomEditSelection | null;
27
+ multiSelectCount?: number;
28
+ copiedAgentPrompt: boolean;
29
+ onClearSelection: () => void;
30
+ onUngroup?: () => void;
31
+ onSetStyle: (prop: string, value: string) => void | Promise<void>;
32
+ onSetAttribute: (attr: string, value: string) => void | Promise<void>;
33
+ onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
34
+ onApplyColorGradingScope?: (
35
+ scope: "source-file" | "project",
36
+ value: string | null,
37
+ ) => Promise<{ changedFiles: number; changedElements: number }>;
38
+ onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
39
+ onRemoveBackground?: (
40
+ inputPath: string,
41
+ options: {
42
+ createBackgroundPlate?: boolean;
43
+ quality?: "fast" | "balanced" | "best";
44
+ onProgress?: (progress: BackgroundRemovalProgress) => void;
45
+ },
46
+ ) => Promise<BackgroundRemovalResult>;
47
+ onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
48
+ onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
49
+ onSetManualRotation: (element: DomEditSelection, next: { angle: number }) => void;
50
+ onSetText: (value: string, fieldKey?: string) => void;
51
+ onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
52
+ onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
53
+ onRemoveTextField: (fieldKey: string) => void;
54
+ onAskAgent: () => void;
55
+ onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
56
+ fontAssets?: ImportedFontAsset[];
57
+ onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
58
+ previewIframeRef?: RefObject<HTMLIFrameElement | null>;
59
+ gsapAnimations?: GsapAnimation[];
60
+ gsapMultipleTimelines?: boolean;
61
+ gsapUnsupportedTimelinePattern?: boolean;
62
+ onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
63
+ onUpdateGsapMeta?: (
64
+ animId: string,
65
+ updates: { duration?: number; ease?: string; position?: number },
66
+ ) => void;
67
+ onDeleteGsapAnimation?: (animId: string) => void;
68
+ onAddGsapProperty?: (animId: string, prop: string) => void;
69
+ onRemoveGsapProperty?: (animId: string, prop: string) => void;
70
+ onUpdateGsapFromProperty?: (animId: string, prop: string, value: number | string) => void;
71
+ onAddGsapFromProperty?: (animId: string, prop: string) => void;
72
+ onRemoveGsapFromProperty?: (animId: string, prop: string) => void;
73
+ onAddGsapAnimation?: (method: "to" | "from" | "set" | "fromTo") => void;
74
+ onSetArcPath?: (
75
+ animId: string,
76
+ config: {
77
+ enabled: boolean;
78
+ autoRotate?: boolean | number;
79
+ segments?: ArcPathSegment[];
80
+ },
81
+ ) => void;
82
+ onUpdateArcSegment?: (
83
+ animId: string,
84
+ segmentIndex: number,
85
+ update: Partial<ArcPathSegment>,
86
+ ) => void;
87
+ onUnroll?: (animationId: string) => void;
88
+ onAddKeyframe?: (
89
+ animationId: string,
90
+ percentage: number,
91
+ property: string,
92
+ value: number | string,
93
+ ) => void;
94
+ onRemoveKeyframe?: (animationId: string, percentage: number) => void;
95
+ onUpdateKeyframeEase?: (animationId: string, percentage: number, ease: string) => void;
96
+ onSetAllKeyframeEases?: (animationId: string, ease: string) => void;
97
+ onConvertToKeyframes?: (animationId: string, duration?: number) => void;
98
+ onCommitAnimatedProperty?: (
99
+ selection: DomEditSelection,
100
+ property: string,
101
+ value: number | string,
102
+ ) => Promise<void>;
103
+ onCommitAnimatedProperties?: (
104
+ selection: DomEditSelection,
105
+ props: Record<string, number | string>,
106
+ ) => Promise<void>;
107
+ onSeekToTime?: (time: number) => void;
108
+ recordingState?: "idle" | "recording" | "preview";
109
+ recordingDuration?: number;
110
+ onToggleRecording?: () => void;
111
+ }
@@ -33,8 +33,9 @@ export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcru
33
33
  });
34
34
  onNavigate(stack.length - 2);
35
35
  }}
36
- className="flex items-center gap-1 px-1.5 py-0.5 rounded text-xs text-neutral-400 hover:text-white hover:bg-neutral-800 transition-colors"
37
- title="Back (Esc)"
36
+ className="flex items-center gap-1 px-1.5 py-0.5 rounded text-xs text-neutral-400 hover:text-white hover:bg-neutral-800 active:scale-[0.98] transition-colors"
37
+ title="Back (Esc, or double-click empty timeline)"
38
+ aria-label="Back to parent composition"
38
39
  >
39
40
  <ArrowLeft size={12} weight="bold" />
40
41
  </button>