@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.
- package/dist/assets/hyperframes-player-BBrKzTGd.js +459 -0
- package/dist/assets/index-BLRTwY5l.js +396 -0
- package/dist/assets/{index-BXYJbIax.js → index-ClUipc8i.js} +1 -1
- package/dist/assets/index-DJaiR8T2.css +1 -0
- package/dist/assets/{index-Ct-X2wlf.js → index-Ykq7ihge.js} +1 -1
- package/dist/index.d.ts +35 -10
- package/dist/index.html +2 -2
- package/dist/index.js +5146 -3321
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +19 -18
- package/src/components/AskAgentModal.tsx +23 -6
- package/src/components/LintModal.tsx +40 -10
- package/src/components/MediaPreview.tsx +36 -1
- package/src/components/SaveQueuePausedBanner.tsx +7 -6
- package/src/components/StudioErrorBoundary.tsx +19 -31
- package/src/components/StudioFeedbackBar.tsx +11 -2
- package/src/components/StudioGlobalDragOverlay.tsx +1 -1
- package/src/components/StudioHeader.tsx +184 -106
- package/src/components/StudioLeftSidebar.tsx +24 -3
- package/src/components/StudioOverlays.tsx +34 -8
- package/src/components/StudioRightPanel.tsx +154 -9
- package/src/components/StudioSplash.tsx +6 -3
- package/src/components/StudioToast.tsx +12 -11
- package/src/components/TimelineToolbar.test.tsx +9 -3
- package/src/components/TimelineToolbar.tsx +63 -29
- package/src/components/editor/PropertyPanel.tsx +18 -17
- package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
- package/src/components/editor/colorGradingScopePatch.ts +57 -0
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +0 -6
- package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
- package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
- package/src/components/editor/propertyPanelHelpers.ts +22 -91
- package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
- package/src/components/editor/propertyPanelTypes.ts +111 -0
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
- package/src/components/nle/NLELayout.tsx +54 -51
- package/src/components/nle/NLEPreview.tsx +41 -5
- package/src/components/nle/TimelineResizeDivider.tsx +97 -0
- package/src/components/nle/usePreviewBlockDrop.ts +31 -7
- package/src/components/renders/RenderQueue.tsx +125 -24
- package/src/components/renders/RenderQueueItem.tsx +132 -64
- package/src/components/renders/useRenderQueue.ts +136 -24
- package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
- package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
- package/src/components/storyboard/StoryboardView.tsx +6 -0
- package/src/components/studioColorGradingScope.ts +124 -0
- package/src/components/studioMediaJobs.ts +123 -0
- package/src/components/ui/Button.tsx +20 -11
- package/src/components/ui/HyperframesLoader.tsx +9 -2
- package/src/components/ui/SearchInput.tsx +53 -0
- package/src/components/ui/Tooltip.tsx +52 -6
- package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
- package/src/components/ui/useDialogBehavior.ts +83 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +7 -0
- package/src/hooks/useCompositionContentLoader.ts +40 -0
- package/src/hooks/useEditorSave.ts +14 -0
- package/src/hooks/useFileManager.ts +39 -25
- package/src/hooks/useFrameCapture.ts +12 -1
- package/src/hooks/usePanelLayout.ts +1 -0
- package/src/hooks/usePreviewPersistence.ts +24 -15
- package/src/hooks/useStudioContextValue.ts +5 -0
- package/src/hooks/useToast.ts +66 -13
- package/src/styles/studio.css +61 -0
- package/src/utils/sdkResolverShadow.test.ts +53 -3
- package/src/utils/sdkResolverShadow.ts +41 -1
- package/src/utils/studioPendingEdits.test.ts +43 -0
- package/src/utils/studioPendingEdits.ts +45 -0
- package/src/utils/studioUiPreferences.ts +4 -0
- package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
- package/dist/assets/index-DmkOvZns.css +0 -1
- package/dist/assets/index-SYSbQyK4.js +0 -396
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useCallback,
|
|
3
|
+
useEffect,
|
|
3
4
|
useMemo,
|
|
4
5
|
useRef,
|
|
5
6
|
useState,
|
|
@@ -27,6 +28,13 @@ import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
|
27
28
|
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
28
29
|
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
29
30
|
import { usePlayerStore } from "../player";
|
|
31
|
+
import { waitForMediaJob } from "./studioMediaJobs";
|
|
32
|
+
import {
|
|
33
|
+
applyColorGradingScopeUpdate,
|
|
34
|
+
EMPTY_COLOR_GRADING_SCOPE_RESULT,
|
|
35
|
+
type ColorGradingScope,
|
|
36
|
+
} from "./studioColorGradingScope";
|
|
37
|
+
import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
|
|
30
38
|
|
|
31
39
|
const MIN_INSPECTOR_SPLIT_PERCENT = 20;
|
|
32
40
|
const MAX_INSPECTOR_SPLIT_PERCENT = 75;
|
|
@@ -69,6 +77,7 @@ export function StudioRightPanel({
|
|
|
69
77
|
}: StudioRightPanelProps) {
|
|
70
78
|
const {
|
|
71
79
|
rightWidth,
|
|
80
|
+
setRightWidth,
|
|
72
81
|
rightPanelTab,
|
|
73
82
|
setRightPanelTab,
|
|
74
83
|
rightInspectorPanes,
|
|
@@ -82,6 +91,7 @@ export function StudioRightPanel({
|
|
|
82
91
|
previewIframeRef,
|
|
83
92
|
projectId,
|
|
84
93
|
activeCompPath,
|
|
94
|
+
showToast,
|
|
85
95
|
compositionDimensions,
|
|
86
96
|
waitForPendingDomEditSaves,
|
|
87
97
|
renderQueue,
|
|
@@ -136,8 +146,10 @@ export function StudioRightPanel({
|
|
|
136
146
|
projectDir,
|
|
137
147
|
handleImportFiles,
|
|
138
148
|
handleImportFonts,
|
|
149
|
+
refreshFileTree,
|
|
139
150
|
readProjectFile,
|
|
140
151
|
writeProjectFile,
|
|
152
|
+
fileTree,
|
|
141
153
|
} = useFileManagerContext();
|
|
142
154
|
|
|
143
155
|
// Discrete ops (toggle, reorder, add/delete, hotspot): persist immediately,
|
|
@@ -172,6 +184,14 @@ export function StudioRightPanel({
|
|
|
172
184
|
startPercent: number;
|
|
173
185
|
height: number;
|
|
174
186
|
} | null>(null);
|
|
187
|
+
const backgroundRemovalAbortRef = useRef<AbortController | null>(null);
|
|
188
|
+
|
|
189
|
+
useEffect(
|
|
190
|
+
() => () => {
|
|
191
|
+
backgroundRemovalAbortRef.current?.abort();
|
|
192
|
+
},
|
|
193
|
+
[],
|
|
194
|
+
);
|
|
175
195
|
|
|
176
196
|
const renderJobs = renderQueue.jobs as RenderJob[];
|
|
177
197
|
const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
|
|
@@ -233,6 +253,90 @@ export function StudioRightPanel({
|
|
|
233
253
|
splitDragRef.current = null;
|
|
234
254
|
}, []);
|
|
235
255
|
|
|
256
|
+
const handleApplyColorGradingScope = useCallback(
|
|
257
|
+
async (scope: ColorGradingScope, value: string | null) =>
|
|
258
|
+
applyColorGradingScopeUpdate({
|
|
259
|
+
scope,
|
|
260
|
+
value,
|
|
261
|
+
selectedSourceFile: domEditSelection?.sourceFile || activeCompPath || "index.html",
|
|
262
|
+
fileTree,
|
|
263
|
+
projectId,
|
|
264
|
+
domEditSaveTimestampRef,
|
|
265
|
+
waitForPendingDomEditSaves,
|
|
266
|
+
readProjectFile,
|
|
267
|
+
writeProjectFile,
|
|
268
|
+
recordEdit,
|
|
269
|
+
reloadPreview,
|
|
270
|
+
showToast,
|
|
271
|
+
}).catch((error) => {
|
|
272
|
+
showToast(
|
|
273
|
+
`Couldn't apply color grading: ${error instanceof Error ? error.message : String(error)}`,
|
|
274
|
+
"error",
|
|
275
|
+
);
|
|
276
|
+
return EMPTY_COLOR_GRADING_SCOPE_RESULT;
|
|
277
|
+
}),
|
|
278
|
+
[
|
|
279
|
+
activeCompPath,
|
|
280
|
+
domEditSaveTimestampRef,
|
|
281
|
+
domEditSelection?.sourceFile,
|
|
282
|
+
fileTree,
|
|
283
|
+
projectId,
|
|
284
|
+
readProjectFile,
|
|
285
|
+
recordEdit,
|
|
286
|
+
reloadPreview,
|
|
287
|
+
showToast,
|
|
288
|
+
waitForPendingDomEditSaves,
|
|
289
|
+
writeProjectFile,
|
|
290
|
+
],
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const handleRemoveBackground = useCallback(
|
|
294
|
+
// fallow-ignore-next-line complexity
|
|
295
|
+
async (
|
|
296
|
+
inputPath: string,
|
|
297
|
+
options: {
|
|
298
|
+
createBackgroundPlate?: boolean;
|
|
299
|
+
quality?: "fast" | "balanced" | "best";
|
|
300
|
+
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
|
301
|
+
},
|
|
302
|
+
) => {
|
|
303
|
+
const response = await fetch(
|
|
304
|
+
`/api/projects/${encodeURIComponent(projectId)}/media/remove-background`,
|
|
305
|
+
{
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: { "Content-Type": "application/json" },
|
|
308
|
+
body: JSON.stringify({
|
|
309
|
+
inputPath,
|
|
310
|
+
createBackgroundPlate: options.createBackgroundPlate === true,
|
|
311
|
+
quality: options.quality ?? "balanced",
|
|
312
|
+
}),
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
const data = (await response.json().catch(() => ({}))) as {
|
|
316
|
+
jobId?: string;
|
|
317
|
+
error?: string;
|
|
318
|
+
};
|
|
319
|
+
if (!response.ok || !data.jobId) {
|
|
320
|
+
throw new Error(data.error || `Background removal failed (${response.status})`);
|
|
321
|
+
}
|
|
322
|
+
showToast("Removing background...", "info");
|
|
323
|
+
backgroundRemovalAbortRef.current?.abort();
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
backgroundRemovalAbortRef.current = controller;
|
|
326
|
+
try {
|
|
327
|
+
const result = await waitForMediaJob(data.jobId, options.onProgress, controller.signal);
|
|
328
|
+
await refreshFileTree();
|
|
329
|
+
showToast(`Created transparent asset: ${result.outputPath.split("/").pop()}`, "info");
|
|
330
|
+
return result;
|
|
331
|
+
} finally {
|
|
332
|
+
if (backgroundRemovalAbortRef.current === controller) {
|
|
333
|
+
backgroundRemovalAbortRef.current = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
[projectId, refreshFileTree, showToast],
|
|
338
|
+
);
|
|
339
|
+
|
|
236
340
|
const propertyPanel = (
|
|
237
341
|
<PropertyPanel
|
|
238
342
|
projectId={projectId}
|
|
@@ -246,7 +350,9 @@ export function StudioRightPanel({
|
|
|
246
350
|
onSetStyle={handleDomStyleCommit}
|
|
247
351
|
onSetAttribute={handleDomAttributeCommit}
|
|
248
352
|
onSetAttributeLive={handleDomAttributeLiveCommit}
|
|
353
|
+
onApplyColorGradingScope={handleApplyColorGradingScope}
|
|
249
354
|
onSetHtmlAttribute={handleDomHtmlAttributeCommit}
|
|
355
|
+
onRemoveBackground={handleRemoveBackground}
|
|
250
356
|
onSetManualOffset={handleDomPathOffsetCommit}
|
|
251
357
|
onSetManualSize={handleDomBoxSizeCommit}
|
|
252
358
|
onSetManualRotation={handleDomRotationCommit}
|
|
@@ -295,6 +401,11 @@ export function StudioRightPanel({
|
|
|
295
401
|
jobs={renderJobs}
|
|
296
402
|
projectId={projectId}
|
|
297
403
|
onDelete={renderQueue.deleteRender}
|
|
404
|
+
onCancel={renderQueue.cancelRender}
|
|
405
|
+
loadError={renderQueue.loadError}
|
|
406
|
+
onRetryLoad={renderQueue.reloadRenders}
|
|
407
|
+
actionError={renderQueue.actionError}
|
|
408
|
+
onDismissActionError={renderQueue.dismissActionError}
|
|
298
409
|
onClearCompleted={renderQueue.clearCompleted}
|
|
299
410
|
onStartRender={async (format, quality, resolution, fps) => {
|
|
300
411
|
await waitForPendingDomEditSaves();
|
|
@@ -316,30 +427,43 @@ export function StudioRightPanel({
|
|
|
316
427
|
return (
|
|
317
428
|
<>
|
|
318
429
|
<div
|
|
319
|
-
|
|
430
|
+
role="separator"
|
|
431
|
+
aria-label="Resize inspector panel"
|
|
432
|
+
aria-orientation="vertical"
|
|
433
|
+
tabIndex={0}
|
|
434
|
+
className="group w-2 flex-shrink-0 cursor-col-resize flex items-center justify-center outline-none focus-visible:bg-studio-accent/20"
|
|
320
435
|
style={{ touchAction: "none" }}
|
|
321
436
|
onPointerDown={(e) => handlePanelResizeStart("right", e)}
|
|
322
437
|
onPointerMove={handlePanelResizeMove}
|
|
323
438
|
onPointerUp={handlePanelResizeEnd}
|
|
439
|
+
onPointerCancel={handlePanelResizeEnd}
|
|
440
|
+
onKeyDown={(e) => {
|
|
441
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
442
|
+
e.preventDefault();
|
|
443
|
+
// Panel is right-anchored: ArrowLeft grows it, ArrowRight shrinks it.
|
|
444
|
+
const delta = e.key === "ArrowLeft" ? 16 : -16;
|
|
445
|
+
setRightWidth(Math.max(160, Math.min(600, rightWidth + delta)));
|
|
446
|
+
}}
|
|
324
447
|
>
|
|
325
448
|
<div className="h-[52px] w-px bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
|
|
326
449
|
</div>
|
|
327
450
|
<div
|
|
328
|
-
className="flex flex-col border-l border-neutral-800 bg-neutral-900
|
|
451
|
+
className="flex min-w-0 flex-shrink-0 flex-col overflow-hidden border-l border-neutral-800 bg-neutral-900"
|
|
329
452
|
style={{ width: rightWidth }}
|
|
330
453
|
>
|
|
331
454
|
{captionEditMode ? (
|
|
332
455
|
<CaptionPropertyPanel iframeRef={previewIframeRef} />
|
|
333
456
|
) : (
|
|
334
457
|
<>
|
|
335
|
-
<div className="flex items-center gap-1 border-b border-neutral-800 px-3 py-2">
|
|
458
|
+
<div className="flex min-w-0 items-center gap-1 overflow-hidden border-b border-neutral-800 px-3 py-2">
|
|
336
459
|
{STUDIO_INSPECTOR_PANELS_ENABLED && (
|
|
337
460
|
<>
|
|
338
461
|
<Tooltip label="Element styles and properties" side="bottom">
|
|
339
462
|
<button
|
|
340
463
|
type="button"
|
|
341
464
|
onClick={() => handleInspectorPaneButtonClick("design")}
|
|
342
|
-
|
|
465
|
+
aria-pressed={designPaneOpen}
|
|
466
|
+
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors active:scale-[0.98] ${
|
|
343
467
|
designPaneOpen
|
|
344
468
|
? "bg-neutral-800 text-white"
|
|
345
469
|
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
@@ -352,7 +476,8 @@ export function StudioRightPanel({
|
|
|
352
476
|
<button
|
|
353
477
|
type="button"
|
|
354
478
|
onClick={() => handleInspectorPaneButtonClick("layers")}
|
|
355
|
-
|
|
479
|
+
aria-pressed={layersPaneOpen}
|
|
480
|
+
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors active:scale-[0.98] ${
|
|
356
481
|
layersPaneOpen
|
|
357
482
|
? "bg-neutral-800 text-white"
|
|
358
483
|
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
@@ -367,7 +492,8 @@ export function StudioRightPanel({
|
|
|
367
492
|
<button
|
|
368
493
|
type="button"
|
|
369
494
|
onClick={() => setRightPanelTab("renders")}
|
|
370
|
-
|
|
495
|
+
aria-pressed={rightPanelTab === "renders"}
|
|
496
|
+
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors active:scale-[0.98] ${
|
|
371
497
|
rightPanelTab === "renders"
|
|
372
498
|
? "bg-neutral-800 text-white"
|
|
373
499
|
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
@@ -380,7 +506,8 @@ export function StudioRightPanel({
|
|
|
380
506
|
<button
|
|
381
507
|
type="button"
|
|
382
508
|
onClick={() => setRightPanelTab("slideshow")}
|
|
383
|
-
|
|
509
|
+
aria-pressed={rightPanelTab === "slideshow"}
|
|
510
|
+
className={`h-8 rounded-xl px-3 text-[11px] font-medium transition-colors active:scale-[0.98] ${
|
|
384
511
|
rightPanelTab === "slideshow"
|
|
385
512
|
? "bg-neutral-800 text-white"
|
|
386
513
|
: "text-neutral-500 hover:bg-neutral-800/70 hover:text-neutral-200"
|
|
@@ -390,7 +517,7 @@ export function StudioRightPanel({
|
|
|
390
517
|
</button>
|
|
391
518
|
</Tooltip>
|
|
392
519
|
</div>
|
|
393
|
-
<div className="min-h-0 flex-1">
|
|
520
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
394
521
|
{rightPanelTab === "block-params" && activeBlockParams ? (
|
|
395
522
|
<BlockParamsPanel
|
|
396
523
|
blockName={activeBlockParams.blockName}
|
|
@@ -406,7 +533,7 @@ export function StudioRightPanel({
|
|
|
406
533
|
onPersistNotes={onPersistSlideshowNotes}
|
|
407
534
|
/>
|
|
408
535
|
) : layersPaneOpen && designPaneOpen ? (
|
|
409
|
-
<div ref={splitContainerRef} className="flex h-full min-h-0 flex-col">
|
|
536
|
+
<div ref={splitContainerRef} className="flex h-full min-h-0 min-w-0 flex-col">
|
|
410
537
|
<div
|
|
411
538
|
className="min-h-[120px] overflow-hidden"
|
|
412
539
|
style={{ flexBasis: `${layersPanePercent}%`, flexShrink: 0 }}
|
|
@@ -432,6 +559,24 @@ export function StudioRightPanel({
|
|
|
432
559
|
<LayersPanel />
|
|
433
560
|
) : designPaneOpen ? (
|
|
434
561
|
propertyPanel
|
|
562
|
+
) : inspectorTabActive ? (
|
|
563
|
+
// Inspector tab selected but no pane can render (panes toggled
|
|
564
|
+
// off, or inspector inactive during playback/recording): show an
|
|
565
|
+
// explanation instead of silently rendering the render queue
|
|
566
|
+
// under a highlighted inspector tab.
|
|
567
|
+
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
|
|
568
|
+
<p className="text-xs text-neutral-500">
|
|
569
|
+
Inspector is unavailable right now — select the Design or Layers pane above, or
|
|
570
|
+
pause playback/recording to inspect elements.
|
|
571
|
+
</p>
|
|
572
|
+
<button
|
|
573
|
+
type="button"
|
|
574
|
+
onClick={() => setRightPanelTab("renders")}
|
|
575
|
+
className="h-7 rounded-md border border-neutral-800 px-3 text-[11px] font-medium text-neutral-400 transition-colors hover:border-neutral-700 hover:text-neutral-200 active:scale-[0.98]"
|
|
576
|
+
>
|
|
577
|
+
Show Renders
|
|
578
|
+
</button>
|
|
579
|
+
</div>
|
|
435
580
|
) : (
|
|
436
581
|
renderQueuePanel
|
|
437
582
|
)}
|
|
@@ -2,15 +2,18 @@ export function StudioSplash({ waiting }: { waiting?: boolean }) {
|
|
|
2
2
|
return (
|
|
3
3
|
<div className="h-full w-full bg-neutral-950 flex items-center justify-center">
|
|
4
4
|
{waiting ? (
|
|
5
|
-
<div className="flex flex-col items-center gap-3 text-center px-6">
|
|
6
|
-
<div className="w-4 h-4 rounded-full border-2 border-neutral-700 border-t-neutral-500 animate-spin" />
|
|
5
|
+
<div className="flex flex-col items-center gap-3 text-center px-6" role="status">
|
|
6
|
+
<div className="w-4 h-4 rounded-full border-2 border-neutral-700 border-t-neutral-500 animate-spin motion-reduce:animate-none" />
|
|
7
7
|
<p className="text-xs text-neutral-600">
|
|
8
8
|
Waiting for preview server… run{" "}
|
|
9
9
|
<code className="text-neutral-500 font-mono">npm run dev</code>
|
|
10
10
|
</p>
|
|
11
11
|
</div>
|
|
12
12
|
) : (
|
|
13
|
-
<div className="
|
|
13
|
+
<div className="flex flex-col items-center gap-3 text-center px-6" role="status">
|
|
14
|
+
<div className="w-4 h-4 rounded-full bg-studio-accent animate-pulse motion-reduce:animate-none" />
|
|
15
|
+
<p className="text-xs text-neutral-600">Connecting to project…</p>
|
|
16
|
+
</div>
|
|
14
17
|
)}
|
|
15
18
|
</div>
|
|
16
19
|
);
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
interface StudioToastProps {
|
|
2
2
|
message: string;
|
|
3
3
|
tone?: "error" | "info";
|
|
4
|
+
/** Plays the exit animation when true (owner removes the node after ~160ms). */
|
|
5
|
+
leaving?: boolean;
|
|
4
6
|
onDismiss?: () => void;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
|
-
export function StudioToast({ message, tone, onDismiss }: StudioToastProps) {
|
|
9
|
+
export function StudioToast({ message, tone, leaving, onDismiss }: StudioToastProps) {
|
|
8
10
|
const isError = tone === "error";
|
|
9
11
|
return (
|
|
10
12
|
<div
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
role={onDismiss ? "button" : undefined}
|
|
14
|
-
style={onDismiss ? { cursor: "pointer" } : undefined}
|
|
13
|
+
role={isError ? "alert" : "status"}
|
|
14
|
+
className={`motion-reduce:animate-none ${leaving ? "hf-toast-exit" : "hf-toast-enter"}`}
|
|
15
15
|
>
|
|
16
16
|
<div
|
|
17
|
-
className="relative flex items-center gap-3 overflow-hidden rounded-2xl pl-4 pr-2
|
|
17
|
+
className="relative flex max-w-[min(420px,calc(100vw-48px))] items-center gap-3 overflow-hidden rounded-2xl py-3 pl-4 pr-2 text-[12px]"
|
|
18
18
|
style={{
|
|
19
19
|
background: isError
|
|
20
20
|
? "linear-gradient(135deg, rgba(127,29,29,0.55), rgba(80,10,10,0.45))"
|
|
@@ -29,14 +29,15 @@ export function StudioToast({ message, tone, onDismiss }: StudioToastProps) {
|
|
|
29
29
|
].join(", "),
|
|
30
30
|
}}
|
|
31
31
|
>
|
|
32
|
-
<span
|
|
32
|
+
<span
|
|
33
|
+
className={`min-w-0 break-words leading-5 ${isError ? "text-red-200" : "text-neutral-200"}`}
|
|
34
|
+
>
|
|
35
|
+
{message}
|
|
36
|
+
</span>
|
|
33
37
|
{onDismiss && (
|
|
34
38
|
<button
|
|
35
39
|
type="button"
|
|
36
|
-
onClick={
|
|
37
|
-
e.stopPropagation();
|
|
38
|
-
onDismiss();
|
|
39
|
-
}}
|
|
40
|
+
onClick={onDismiss}
|
|
40
41
|
className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-white/10 hover:text-neutral-300"
|
|
41
42
|
aria-label="Dismiss"
|
|
42
43
|
>
|
|
@@ -30,21 +30,27 @@ function renderToolbar() {
|
|
|
30
30
|
describe("TimelineToolbar — auto-keyframe toggle (#1808)", () => {
|
|
31
31
|
it("renders enabled (pressed) by default with no selection", () => {
|
|
32
32
|
const { host, root } = renderToolbar();
|
|
33
|
-
const btn = host.querySelector<HTMLButtonElement>(
|
|
33
|
+
const btn = host.querySelector<HTMLButtonElement>(
|
|
34
|
+
'button[aria-label="Auto-record manual edits as keyframes"]',
|
|
35
|
+
);
|
|
34
36
|
expect(btn).not.toBeNull();
|
|
37
|
+
expect(btn?.getAttribute("aria-pressed")).toBe("true");
|
|
35
38
|
act(() => root.unmount());
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
it("flips autoKeyframeEnabled in the store when clicked", () => {
|
|
39
42
|
const { host, root } = renderToolbar();
|
|
40
|
-
const btn = host.querySelector<HTMLButtonElement>(
|
|
43
|
+
const btn = host.querySelector<HTMLButtonElement>(
|
|
44
|
+
'button[aria-label="Auto-record manual edits as keyframes"]',
|
|
45
|
+
);
|
|
46
|
+
if (!btn) throw new Error("auto-keyframe toggle not rendered");
|
|
41
47
|
|
|
42
48
|
act(() => {
|
|
43
49
|
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
44
50
|
});
|
|
45
51
|
|
|
46
52
|
expect(usePlayerStore.getState().autoKeyframeEnabled).toBe(false);
|
|
47
|
-
expect(
|
|
53
|
+
expect(btn.getAttribute("aria-pressed")).toBe("false");
|
|
48
54
|
act(() => root.unmount());
|
|
49
55
|
});
|
|
50
56
|
});
|
|
@@ -112,7 +112,9 @@ export function TimelineToolbar({
|
|
|
112
112
|
<button
|
|
113
113
|
type="button"
|
|
114
114
|
onClick={() => setActiveTool("select")}
|
|
115
|
-
|
|
115
|
+
aria-label="Selection tool"
|
|
116
|
+
aria-pressed={activeTool === "select"}
|
|
117
|
+
className={`flex h-6 w-6 items-center justify-center transition-colors active:scale-[0.98] ${
|
|
116
118
|
activeTool === "select"
|
|
117
119
|
? "bg-neutral-700 text-neutral-200"
|
|
118
120
|
: "text-neutral-500 hover:text-neutral-300"
|
|
@@ -123,11 +125,13 @@ export function TimelineToolbar({
|
|
|
123
125
|
</svg>
|
|
124
126
|
</button>
|
|
125
127
|
</Tooltip>
|
|
126
|
-
<Tooltip label="Razor tool (B)">
|
|
128
|
+
<Tooltip label="Razor tool (B) — Shift+click splits all tracks">
|
|
127
129
|
<button
|
|
128
130
|
type="button"
|
|
129
131
|
onClick={() => setActiveTool("razor")}
|
|
130
|
-
|
|
132
|
+
aria-label="Razor tool"
|
|
133
|
+
aria-pressed={activeTool === "razor"}
|
|
134
|
+
className={`flex h-6 w-6 items-center justify-center transition-colors active:scale-[0.98] ${
|
|
131
135
|
activeTool === "razor"
|
|
132
136
|
? "bg-neutral-700 text-neutral-200"
|
|
133
137
|
: "text-neutral-500 hover:text-neutral-300"
|
|
@@ -153,7 +157,12 @@ export function TimelineToolbar({
|
|
|
153
157
|
<button
|
|
154
158
|
type="button"
|
|
155
159
|
onClick={onToggleKeyframe}
|
|
156
|
-
|
|
160
|
+
aria-label={
|
|
161
|
+
keyframeState === "active"
|
|
162
|
+
? "Remove keyframe at playhead"
|
|
163
|
+
: "Add keyframe at playhead"
|
|
164
|
+
}
|
|
165
|
+
className={`flex h-7 w-7 items-center justify-center rounded transition-colors active:scale-[0.98] ${
|
|
157
166
|
keyframeState === "active"
|
|
158
167
|
? "text-studio-accent"
|
|
159
168
|
: keyframeState === "inactive"
|
|
@@ -187,8 +196,9 @@ export function TimelineToolbar({
|
|
|
187
196
|
<button
|
|
188
197
|
type="button"
|
|
189
198
|
onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
|
|
199
|
+
aria-label="Auto-record manual edits as keyframes"
|
|
190
200
|
aria-pressed={autoKeyframeEnabled}
|
|
191
|
-
className={`flex h-7 w-7 items-center justify-center rounded transition-colors ${
|
|
201
|
+
className={`flex h-7 w-7 items-center justify-center rounded transition-colors active:scale-[0.98] ${
|
|
192
202
|
autoKeyframeEnabled
|
|
193
203
|
? "text-red-400 hover:text-red-300"
|
|
194
204
|
: "text-neutral-600 hover:text-neutral-400"
|
|
@@ -213,23 +223,35 @@ export function TimelineToolbar({
|
|
|
213
223
|
)}
|
|
214
224
|
{onSplitElement &&
|
|
215
225
|
(() => {
|
|
226
|
+
// Render the button unconditionally (disabled when unusable):
|
|
227
|
+
// mounting/unmounting mid-task shifts the neighboring controls.
|
|
216
228
|
const { selectedElementId, elements, currentTime } = usePlayerStore.getState();
|
|
217
229
|
const el = selectedElementId
|
|
218
230
|
? elements.find((e) => (e.key ?? e.id) === selectedElementId)
|
|
219
231
|
: null;
|
|
220
|
-
|
|
221
|
-
const canSplit =
|
|
232
|
+
const splittable = el != null && canSplitElement(el);
|
|
233
|
+
const canSplit =
|
|
234
|
+
splittable && currentTime > el.start && currentTime < el.start + el.duration;
|
|
222
235
|
return (
|
|
223
|
-
<Tooltip
|
|
236
|
+
<Tooltip
|
|
237
|
+
label={
|
|
238
|
+
canSplit
|
|
239
|
+
? "Split clip at playhead (S)"
|
|
240
|
+
: splittable
|
|
241
|
+
? "Move the playhead inside the clip to split"
|
|
242
|
+
: "Select a clip to split"
|
|
243
|
+
}
|
|
244
|
+
>
|
|
224
245
|
<button
|
|
225
246
|
type="button"
|
|
226
247
|
disabled={!canSplit}
|
|
248
|
+
aria-label="Split clip at playhead"
|
|
227
249
|
onClick={() => {
|
|
228
|
-
if (canSplit) onSplitElement(el, currentTime);
|
|
250
|
+
if (canSplit && el) onSplitElement(el, currentTime);
|
|
229
251
|
}}
|
|
230
252
|
className={`flex h-7 w-7 items-center justify-center rounded transition-colors ${
|
|
231
253
|
canSplit
|
|
232
|
-
? "text-neutral-500 hover:text-neutral-200"
|
|
254
|
+
? "text-neutral-500 hover:text-neutral-200 active:scale-[0.98]"
|
|
233
255
|
: "text-neutral-700 cursor-not-allowed"
|
|
234
256
|
}`}
|
|
235
257
|
>
|
|
@@ -239,26 +261,38 @@ export function TimelineToolbar({
|
|
|
239
261
|
);
|
|
240
262
|
})()}
|
|
241
263
|
{beatAnalysisReady &&
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
onClick={() => addBeatAtCompositionTime(currentTime)}
|
|
248
|
-
className="flex h-7 w-7 items-center justify-center rounded text-neutral-500 transition-colors hover:text-[#22c55e]"
|
|
264
|
+
(() => {
|
|
265
|
+
const canAdd = canAddBeatAt(currentTime);
|
|
266
|
+
return (
|
|
267
|
+
<Tooltip
|
|
268
|
+
label={canAdd ? "Add beat at playhead" : "A beat already exists at the playhead"}
|
|
249
269
|
>
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
disabled={!canAdd}
|
|
273
|
+
aria-label="Add beat at playhead"
|
|
274
|
+
onClick={() => {
|
|
275
|
+
if (canAdd) addBeatAtCompositionTime(currentTime);
|
|
276
|
+
}}
|
|
277
|
+
className={`flex h-7 w-7 items-center justify-center rounded transition-colors ${
|
|
278
|
+
canAdd
|
|
279
|
+
? "text-neutral-500 hover:text-[#22c55e] active:scale-[0.98]"
|
|
280
|
+
: "text-neutral-700 cursor-not-allowed"
|
|
281
|
+
}`}
|
|
282
|
+
>
|
|
283
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
284
|
+
<path
|
|
285
|
+
d="M21 10C21 12.2091 16.9706 14 12 14M21 10C21 7.79086 16.9706 6 12 6C7.02944 6 3 7.79086 3 10M21 10V16C21 18.2091 16.9706 20 12 20M12 14C7.02944 14 3 12.2091 3 10M12 14V20M3 10V16C3 18.2091 7.02944 20 12 20M7 19.3264V13.3264M17 19.3264V13.3264M12 10L20 4"
|
|
286
|
+
stroke="currentColor"
|
|
287
|
+
strokeWidth="2"
|
|
288
|
+
strokeLinecap="round"
|
|
289
|
+
strokeLinejoin="round"
|
|
290
|
+
/>
|
|
291
|
+
</svg>
|
|
292
|
+
</button>
|
|
293
|
+
</Tooltip>
|
|
294
|
+
);
|
|
295
|
+
})()}
|
|
262
296
|
</div>
|
|
263
297
|
<div className="flex items-center gap-1">
|
|
264
298
|
<Tooltip label="Fit timeline to width">
|
|
@@ -22,11 +22,7 @@ import { TextSection, StyleSections } from "./propertyPanelSections";
|
|
|
22
22
|
import { GsapAnimationSection } from "./GsapAnimationSection";
|
|
23
23
|
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
|
|
24
24
|
import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
25
|
-
import {
|
|
26
|
-
STUDIO_COLOR_GRADING_ENABLED,
|
|
27
|
-
STUDIO_GSAP_PANEL_ENABLED,
|
|
28
|
-
STUDIO_KEYFRAMES_ENABLED,
|
|
29
|
-
} from "./manualEditingAvailability";
|
|
25
|
+
import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
|
|
30
26
|
import { usePlayerStore, liveTime } from "../../player";
|
|
31
27
|
import { TimingSection } from "./propertyPanelTimingSection";
|
|
32
28
|
import { type PropertyPanelProps } from "./propertyPanelHelpers";
|
|
@@ -57,7 +53,9 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
57
53
|
onSetStyle,
|
|
58
54
|
onSetAttribute,
|
|
59
55
|
onSetAttributeLive,
|
|
56
|
+
onApplyColorGradingScope,
|
|
60
57
|
onSetHtmlAttribute,
|
|
58
|
+
onRemoveBackground,
|
|
61
59
|
onSetManualOffset,
|
|
62
60
|
onSetManualSize,
|
|
63
61
|
onSetManualRotation,
|
|
@@ -352,18 +350,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
352
350
|
onSetAttribute={onSetAttribute}
|
|
353
351
|
/>
|
|
354
352
|
)}
|
|
355
|
-
{sections.
|
|
356
|
-
<MediaSection
|
|
357
|
-
projectDir={projectDir}
|
|
358
|
-
element={element}
|
|
359
|
-
styles={styles}
|
|
360
|
-
onSetStyle={onSetStyle}
|
|
361
|
-
onSetAttribute={onSetAttribute}
|
|
362
|
-
onSetHtmlAttribute={onSetHtmlAttribute}
|
|
363
|
-
/>
|
|
364
|
-
)}
|
|
365
|
-
|
|
366
|
-
{STUDIO_COLOR_GRADING_ENABLED && sections.colorGrading && (
|
|
353
|
+
{sections.colorGrading && (
|
|
367
354
|
<ColorGradingSection
|
|
368
355
|
key={[
|
|
369
356
|
element.id ?? "",
|
|
@@ -371,11 +358,25 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
371
358
|
element.selector ?? "",
|
|
372
359
|
String(element.selectorIndex ?? ""),
|
|
373
360
|
].join("|")}
|
|
361
|
+
projectId={projectId}
|
|
374
362
|
element={element}
|
|
375
363
|
assets={assets}
|
|
376
364
|
previewIframeRef={previewIframeRef}
|
|
377
365
|
onImportAssets={onImportAssets}
|
|
378
366
|
onSetAttributeLive={onSetAttributeLive}
|
|
367
|
+
onApplyScope={onApplyColorGradingScope}
|
|
368
|
+
/>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
{sections.media && (
|
|
372
|
+
<MediaSection
|
|
373
|
+
projectDir={projectDir}
|
|
374
|
+
element={element}
|
|
375
|
+
styles={styles}
|
|
376
|
+
onSetStyle={onSetStyle}
|
|
377
|
+
onSetAttribute={onSetAttribute}
|
|
378
|
+
onSetHtmlAttribute={onSetHtmlAttribute}
|
|
379
|
+
onRemoveBackground={onRemoveBackground}
|
|
379
380
|
/>
|
|
380
381
|
)}
|
|
381
382
|
|