@hyperframes/studio 0.8.104 → 0.8.106
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-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
- package/dist/assets/index-CMiYVGv5.js +399 -0
- package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
- package/dist/assets/index-lrT-gfHa.css +1 -0
- package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.js} +1 -1
- package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
- package/dist/chunk-EEXSYY5F.js.map +1 -0
- package/dist/{chunk-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
- package/dist/chunk-ICTZJPQQ.js.map +1 -0
- package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
- package/dist/chunk-NCUBAVGK.js.map +1 -0
- package/dist/index.css +6 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.html +11 -2
- package/dist/index.js +10391 -9836
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.js +2 -2
- package/package.json +9 -9
- package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
- package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
- package/src/captions/components/CaptionTimeline.tsx +3 -3
- package/src/captions/keyboard.test.ts +6 -0
- package/src/captions/keyboard.ts +2 -1
- package/src/components/AskAgentModal.tsx +1 -1
- package/src/components/LintModal.tsx +35 -35
- package/src/components/StudioErrorBoundary.tsx +1 -1
- package/src/components/StudioHeader.dom.test.tsx +35 -13
- package/src/components/StudioHeader.tsx +6 -2
- package/src/components/StudioToast.tsx +7 -16
- package/src/components/ThemeToggle.tsx +99 -0
- package/src/components/TimelineToolPicker.tsx +1 -1
- package/src/components/TimelineToolbar.tsx +11 -11
- package/src/components/dock/Dock.tsx +1 -1
- package/src/components/dock/dock.css +9 -1
- package/src/components/editor/AnimationCard.tsx +5 -5
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/BorderRadiusEditor.tsx +8 -15
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/ComputedTweenNotice.tsx +1 -1
- package/src/components/editor/DomEditOverlay.tsx +37 -5
- package/src/components/editor/DomEditRotateHandle.tsx +1 -1
- package/src/components/editor/EaseCurveSection.tsx +14 -19
- package/src/components/editor/EaseModeControls.tsx +2 -2
- package/src/components/editor/EaseParamFields.tsx +8 -8
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/FileTreeIcons.tsx +47 -26
- package/src/components/editor/FileTreeNodes.tsx +11 -8
- package/src/components/editor/GestureRecordControl.tsx +3 -3
- package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/InlineTextToolbar.tsx +4 -4
- package/src/components/editor/InspectorHeaderActions.tsx +1 -1
- package/src/components/editor/KeyframeDiamond.tsx +6 -4
- package/src/components/editor/KeyframeNavigation.tsx +4 -4
- package/src/components/editor/LayersPanel.tsx +3 -3
- package/src/components/editor/PromotableControl.tsx +2 -2
- package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
- package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
- package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +5 -5
- package/src/components/editor/SourceEditor.tsx +2 -6
- package/src/components/editor/TimelineFxPopover.tsx +4 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
- package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
- package/src/components/editor/easeCurveSvg.tsx +1 -1
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
- package/src/components/editor/manualEditsDom.ts +36 -3
- package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
- package/src/components/editor/manualEditsDomPatches.ts +0 -30
- package/src/components/editor/manualEditsSnapshot.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +69 -1
- package/src/components/editor/marqueeCommit.ts +6 -1
- package/src/components/editor/overlayFrameLoop.test.ts +27 -0
- package/src/components/editor/overlayFrameLoop.ts +21 -0
- package/src/components/editor/persistSeam.integration.test.ts +0 -18
- package/src/components/editor/plainTranslate.ts +34 -3
- package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
- package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
- package/src/components/editor/propertyPanelFill.tsx +6 -6
- package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
- package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
- package/src/components/editor/propertyPanelFont.test.tsx +80 -3
- package/src/components/editor/propertyPanelFont.tsx +71 -48
- package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
- package/src/components/editor/propertyPanelFxControls.tsx +2 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
- package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +1 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
- package/src/components/editor/propertyPanelValueTier.ts +1 -1
- package/src/components/editor/studioCodeTheme.test.ts +20 -0
- package/src/components/editor/studioCodeTheme.ts +99 -0
- package/src/components/editor/useDomEditNudge.test.tsx +31 -0
- package/src/components/editor/useDomEditNudge.ts +2 -2
- package/src/components/editor/useInlineTextEditing.tsx +1 -1
- package/src/components/editor/useMotionPathData.test.tsx +64 -0
- package/src/components/editor/useMotionPathData.ts +5 -1
- package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
- package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
- package/src/components/nle/AudioMeterStrip.tsx +2 -2
- package/src/components/nle/NLEPreview.test.ts +8 -4
- package/src/components/nle/PreviewOverlays.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +3 -3
- package/src/components/nle/PreviewPoster.tsx +29 -1
- package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
- package/src/components/panels/SlideshowPanel.tsx +4 -4
- package/src/components/panels/SlideshowSubPanels.tsx +19 -21
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +4 -6
- package/src/components/panels/VariablesRowAction.tsx +1 -1
- package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
- package/src/components/renders/RenderQueue.tsx +2 -2
- package/src/components/renders/RenderQueueItem.tsx +3 -3
- package/src/components/sidebar/AssetCard.tsx +3 -3
- package/src/components/sidebar/AssetContextMenu.tsx +7 -7
- package/src/components/sidebar/AssetsTab.tsx +6 -6
- package/src/components/sidebar/AudioRow.tsx +5 -3
- package/src/components/sidebar/BlocksTab.tsx +6 -6
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
- package/src/components/sidebar/CompositionsTab.tsx +65 -12
- package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
- package/src/components/sidebar/SidebarLintButton.tsx +3 -3
- package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
- package/src/components/timelineToolbarStyles.ts +3 -3
- package/src/components/ui/Button.tsx +4 -4
- package/src/components/ui/HyperframesLogo.tsx +19 -18
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Menu.tsx +4 -2
- package/src/hooks/appHotkeysDispatch.ts +2 -1
- package/src/hooks/elementOffsetStager.ts +8 -7
- package/src/hooks/previewPromotionSelection.test.tsx +180 -0
- package/src/hooks/useAppHotkeys.test.ts +32 -0
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useClipboard.source.test.ts +141 -23
- package/src/hooks/useClipboard.ts +154 -123
- package/src/hooks/useDomEditCommits.ts +0 -3
- package/src/hooks/useDomEditSession.test.tsx +76 -72
- package/src/hooks/useDomEditSession.ts +7 -43
- package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
- package/src/hooks/useDomGeometryCommit.ts +0 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
- package/src/hooks/useDomGeometryCommits.ts +4 -49
- package/src/hooks/useGsapTweenCache.ts +1 -2
- package/src/hooks/useInlineTextEdit.ts +16 -2
- package/src/hooks/useLivePreviewIframe.ts +18 -0
- package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
- package/src/hooks/usePreviewPersistence.ts +2 -5
- package/src/hooks/useRenderClipContent.ts +3 -3
- package/src/index.ts +2 -0
- package/src/livePreviewIframeExport.test.tsx +33 -0
- package/src/main.tsx +4 -1
- package/src/player/components/AudioWaveform.test.tsx +36 -15
- package/src/player/components/AudioWaveform.tsx +24 -7
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/CompositionThumbnail.tsx +23 -11
- package/src/player/components/EditModal.tsx +6 -6
- package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
- package/src/player/components/PlayerControls.tsx +3 -3
- package/src/player/components/ShortcutsPanel.tsx +8 -20
- package/src/player/components/TimelineCanvas.tsx +1 -1
- package/src/player/components/TimelineClip.test.tsx +38 -0
- package/src/player/components/TimelineClip.tsx +16 -8
- package/src/player/components/TimelineClipFades.test.tsx +376 -74
- package/src/player/components/TimelineClipFades.tsx +368 -137
- package/src/player/components/TimelineEmptyState.tsx +2 -2
- package/src/player/components/TimelineLanes.tsx +0 -1
- package/src/player/components/TrackClipCount.tsx +1 -1
- package/src/player/components/VolumeControl.tsx +1 -1
- package/src/player/components/timelineClipChildren.tsx +1 -1
- package/src/player/components/timelineMotionStyles.test.ts +6 -22
- package/src/player/components/trackHeaderLabelRows.tsx +16 -16
- package/src/player/components/useTimelineKeyboardActor.ts +1 -1
- package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
- package/src/player/hooks/useShadowPreviewReload.ts +72 -5
- package/src/player/lib/automationStoreSync.test.ts +18 -1
- package/src/player/lib/automationStoreSync.ts +29 -17
- package/src/player/lib/thumbnailScheduler.ts +4 -5
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/player/sceneSwap.ts +18 -0
- package/src/styles/components.css +26 -0
- package/src/styles/hex-baseline.json +7 -19
- package/src/styles/studio.css +4 -7
- package/src/styles/tailwind-preset.shared.js +2 -1
- package/src/styles/theme.css +214 -147
- package/src/styles/theme.test.ts +13 -9
- package/src/utils/blockCategories.ts +41 -9
- package/src/utils/clipboardPayload.ts +12 -1
- package/src/utils/studioPendingEdits.ts +21 -0
- package/src/utils/studioTheme.test.tsx +141 -0
- package/src/utils/studioTheme.ts +34 -0
- package/src/utils/studioUiPreferences.ts +3 -0
- package/src/utils/typingTarget.test.ts +8 -1
- package/src/utils/typingTarget.ts +5 -0
- package/dist/assets/index-CQN3M56N.css +0 -1
- package/dist/assets/index-De2aO4XM.js +0 -399
- package/dist/chunk-62DDWOSH.js.map +0 -1
- package/dist/chunk-6GAO75IU.js.map +0 -1
- package/dist/chunk-HIGHVWYD.js.map +0 -1
- package/src/hooks/gsapTargetCache.ts +0 -62
|
@@ -20,24 +20,30 @@ afterEach(() => {
|
|
|
20
20
|
document.body.innerHTML = "";
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
/** A 6 x 20 canvas whose 2D context records every fill as [style, x, y, width, height]. */
|
|
24
|
+
function recordingCanvas() {
|
|
25
|
+
const fills: Array<[string, number, number, number, number]> = [];
|
|
26
|
+
const context = {
|
|
27
|
+
scale: vi.fn(),
|
|
28
|
+
clearRect: vi.fn(),
|
|
29
|
+
fillStyle: "",
|
|
30
|
+
fillRect: (x: number, y: number, width: number, height: number) =>
|
|
31
|
+
fills.push([String(context.fillStyle), x, y, width, height]),
|
|
32
|
+
} as unknown as CanvasRenderingContext2D;
|
|
33
|
+
const canvas = document.createElement("canvas");
|
|
34
|
+
Object.defineProperties(canvas, {
|
|
35
|
+
clientWidth: { value: 6 },
|
|
36
|
+
clientHeight: { value: 20 },
|
|
37
|
+
});
|
|
38
|
+
vi.spyOn(canvas, "getContext").mockReturnValue(context);
|
|
39
|
+
return { canvas, fills };
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
describe("AudioWaveform", () => {
|
|
24
43
|
it("paints a baseline and peak bar for every mapped waveform bin", () => {
|
|
25
|
-
const
|
|
26
|
-
const context = {
|
|
27
|
-
scale: vi.fn(),
|
|
28
|
-
clearRect: vi.fn(),
|
|
29
|
-
fillRect,
|
|
30
|
-
fillStyle: "",
|
|
31
|
-
} as unknown as CanvasRenderingContext2D;
|
|
32
|
-
const canvas = document.createElement("canvas");
|
|
33
|
-
Object.defineProperties(canvas, {
|
|
34
|
-
clientWidth: { value: 6 },
|
|
35
|
-
clientHeight: { value: 20 },
|
|
36
|
-
});
|
|
37
|
-
vi.spyOn(canvas, "getContext").mockReturnValue(context);
|
|
44
|
+
const { canvas, fills } = recordingCanvas();
|
|
38
45
|
drawWaveformCanvas(canvas, [0.25, 1], false, 0, 1);
|
|
39
|
-
expect(
|
|
40
|
-
expect(fillRect.mock.calls.map(([x, y, width, height]) => [x, y, width, height])).toEqual([
|
|
46
|
+
expect(fills.map(([, x, y, width, height]) => [x, y, width, height])).toEqual([
|
|
41
47
|
[0, 18, 3, 2],
|
|
42
48
|
[0, 15, 3, 5],
|
|
43
49
|
[3, 18, 3, 2],
|
|
@@ -45,6 +51,21 @@ describe("AudioWaveform", () => {
|
|
|
45
51
|
]);
|
|
46
52
|
});
|
|
47
53
|
|
|
54
|
+
it("shrinks each bar to the fade's gain and keeps the cut-away part as a ghost", () => {
|
|
55
|
+
const { canvas, fills } = recordingCanvas();
|
|
56
|
+
// A 1 s fade-in on a 2 s clip: the first bar's centre (0.5 s) plays at half gain.
|
|
57
|
+
drawWaveformCanvas(canvas, [1, 1], false, 0, 1, { fadeIn: 1, fadeOut: 0, duration: 2 });
|
|
58
|
+
const bars = fills.filter(([, , y, , height]) => !(y === 18 && height === 2));
|
|
59
|
+
// The heard half, then the ghost only above it, so the two never stack.
|
|
60
|
+
expect(bars.map(([, x, y, width, height]) => [x, y, width, height])).toEqual([
|
|
61
|
+
[0, 10, 3, 10],
|
|
62
|
+
[0, 0, 3, 10],
|
|
63
|
+
[3, 0, 3, 20],
|
|
64
|
+
]);
|
|
65
|
+
const alpha = (style: string) => Number(style.split(",").at(-1)?.replace(")", ""));
|
|
66
|
+
expect(alpha(bars[1][0])).toBeCloseTo(alpha(bars[0][0]) * 0.27, 2);
|
|
67
|
+
});
|
|
68
|
+
|
|
48
69
|
it("leases waveform decoding with the clip's project, session, and viewport priority", () => {
|
|
49
70
|
const host = document.createElement("div");
|
|
50
71
|
document.body.append(host);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
|
1
|
+
import { memo, useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { fadeGain } from "@hyperframes/core/audio-fade";
|
|
2
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
4
|
import { useThumbnailLease } from "../../hooks/useThumbnailLease";
|
|
4
5
|
import { createThumbnailKey, type ThumbnailPriority } from "../lib/thumbnailScheduler";
|
|
5
6
|
import { decimatePeaks, loudnessToOpacity } from "./audioWaveformPeaks";
|
|
7
|
+
import { ClipFadesContext, type ClipFadeShape } from "./TimelineClipFades";
|
|
6
8
|
|
|
7
9
|
export interface AudioWaveformProps {
|
|
8
10
|
audioUrl: string;
|
|
@@ -22,7 +24,10 @@ export interface AudioWaveformProps {
|
|
|
22
24
|
|
|
23
25
|
const BAR_STEP = 3;
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
export const rendersWaveform = (el: { tag: string }) => el.tag === "audio";
|
|
28
|
+
const FADE_GHOST_OPACITY = 0.27;
|
|
29
|
+
|
|
30
|
+
type BarGeometry = { x: number; width: number; height: number; gain: number };
|
|
26
31
|
|
|
27
32
|
function paintWaveformBars(
|
|
28
33
|
context: CanvasRenderingContext2D,
|
|
@@ -33,11 +38,18 @@ function paintWaveformBars(
|
|
|
33
38
|
amplitudes: readonly number[],
|
|
34
39
|
) {
|
|
35
40
|
bars.forEach((bar, index) => {
|
|
36
|
-
const
|
|
41
|
+
const opacity = loudnessToOpacity(amplitudes[index] ?? 0);
|
|
37
42
|
context.fillStyle = `rgb(${waveformBaselineRgb})`;
|
|
38
43
|
context.fillRect(bar.x, height - 2, bar.width, 2);
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
const paint = (alpha: number, top: number, barHeight: number) => {
|
|
45
|
+
context.fillStyle = `rgba(${waveformBarRgb},${alpha.toFixed(2)})`;
|
|
46
|
+
context.fillRect(bar.x, top, bar.width, barHeight);
|
|
47
|
+
};
|
|
48
|
+
const faded = bar.height * bar.gain;
|
|
49
|
+
if (faded > 0) paint(opacity, height - faded, faded);
|
|
50
|
+
if (faded < bar.height) {
|
|
51
|
+
paint(opacity * FADE_GHOST_OPACITY, height - bar.height, bar.height - faded);
|
|
52
|
+
}
|
|
41
53
|
});
|
|
42
54
|
}
|
|
43
55
|
|
|
@@ -47,6 +59,7 @@ export function drawWaveformCanvas(
|
|
|
47
59
|
muted: boolean,
|
|
48
60
|
trimStartFraction: number,
|
|
49
61
|
trimEndFraction: number,
|
|
62
|
+
fades: ClipFadeShape | null = null,
|
|
50
63
|
) {
|
|
51
64
|
const width = Math.max(1, canvas.clientWidth);
|
|
52
65
|
const height = Math.max(1, canvas.clientHeight);
|
|
@@ -67,6 +80,9 @@ export function drawWaveformCanvas(
|
|
|
67
80
|
x: (index * width) / amplitudes.length,
|
|
68
81
|
width: Math.max(1, width / amplitudes.length),
|
|
69
82
|
height: Math.max(3, amplitude * height),
|
|
83
|
+
gain: fades
|
|
84
|
+
? fadeGain(((index + 0.5) / amplitudes.length) * fades.duration, fades.duration, fades)
|
|
85
|
+
: 1,
|
|
70
86
|
}));
|
|
71
87
|
const channelToken = muted ? "--timeline-waveform-muted-rgb" : "--timeline-waveform-bar-rgb";
|
|
72
88
|
const waveformBarRgb = getComputedStyle(canvas).getPropertyValue(channelToken);
|
|
@@ -179,11 +195,12 @@ export const AudioWaveform = memo(function AudioWaveform({
|
|
|
179
195
|
const peaks =
|
|
180
196
|
snapshot.status === "ready" && snapshot.value.kind === "waveform" ? snapshot.value.peaks : null;
|
|
181
197
|
|
|
198
|
+
const fades = useContext(ClipFadesContext);
|
|
182
199
|
const draw = useCallback(() => {
|
|
183
200
|
const canvas = canvasRef.current;
|
|
184
201
|
if (!canvas || !peaks) return;
|
|
185
|
-
drawWaveformCanvas(canvas, peaks, muted, trimStartFraction ?? 0, trimEndFraction ?? 1);
|
|
186
|
-
}, [muted, peaks, trimEndFraction, trimStartFraction]);
|
|
202
|
+
drawWaveformCanvas(canvas, peaks, muted, trimStartFraction ?? 0, trimEndFraction ?? 1, fades);
|
|
203
|
+
}, [fades, muted, peaks, trimEndFraction, trimStartFraction]);
|
|
187
204
|
|
|
188
205
|
const setCanvasRef = useCallback(
|
|
189
206
|
(canvas: HTMLCanvasElement | null) => {
|
|
@@ -201,7 +201,7 @@ export const ClipContextMenu = memo(function ClipContextMenu({
|
|
|
201
201
|
<button
|
|
202
202
|
type="button"
|
|
203
203
|
role="menuitem"
|
|
204
|
-
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-
|
|
204
|
+
className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-danger-ink hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left"
|
|
205
205
|
onClick={() => {
|
|
206
206
|
onDelete(element);
|
|
207
207
|
onClose();
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { memo, useMemo, type CSSProperties } from "react";
|
|
2
2
|
import { useThumbnailLease } from "../../hooks/useThumbnailLease";
|
|
3
3
|
import { useThumbnailStripSize } from "../../hooks/useThumbnailStripSize";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createThumbnailKey,
|
|
6
|
+
type ThumbnailPriority,
|
|
7
|
+
type ThumbnailRequest,
|
|
8
|
+
} from "../lib/thumbnailScheduler";
|
|
5
9
|
import { TIMELINE_VIEWPORT_BUDGETS } from "../lib/timelineViewportBudgets";
|
|
6
10
|
import { computeThumbnailStrip, probeImageAspect } from "./thumbnailUtils";
|
|
7
11
|
|
|
@@ -85,6 +89,22 @@ export function compositionPathOfPreviewUrl(previewUrl: string): string {
|
|
|
85
89
|
return match?.[1] ? decodeURIComponent(match[1]) : "index.html";
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
export function compositionThumbnailRequest(
|
|
93
|
+
url: string,
|
|
94
|
+
projectId: string,
|
|
95
|
+
{ sessionEpoch = 0, priority = "visible", rich = false }: Partial<ThumbnailRequest> = {},
|
|
96
|
+
): ThumbnailRequest {
|
|
97
|
+
return {
|
|
98
|
+
key: createThumbnailKey({ kind: "composition", url }),
|
|
99
|
+
projectId,
|
|
100
|
+
sessionEpoch,
|
|
101
|
+
kind: "composition",
|
|
102
|
+
priority,
|
|
103
|
+
rich,
|
|
104
|
+
load: (signal: AbortSignal) => loadCompositionImage(url, signal),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
async function loadCompositionImage(url: string, signal: AbortSignal) {
|
|
89
109
|
const response = await fetch(url, { signal });
|
|
90
110
|
if (!response.ok) throw new Error(`Composition thumbnail failed (${response.status})`);
|
|
@@ -132,15 +152,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
|
|
|
132
152
|
contentRevision,
|
|
133
153
|
});
|
|
134
154
|
const request = useMemo(
|
|
135
|
-
() => ({
|
|
136
|
-
key: createThumbnailKey({ kind: "composition", url }),
|
|
137
|
-
projectId,
|
|
138
|
-
sessionEpoch,
|
|
139
|
-
kind: "composition" as const,
|
|
140
|
-
priority,
|
|
141
|
-
rich: true,
|
|
142
|
-
load: (signal: AbortSignal) => loadCompositionImage(url, signal),
|
|
143
|
-
}),
|
|
155
|
+
() => compositionThumbnailRequest(url, projectId, { sessionEpoch, priority, rich: true }),
|
|
144
156
|
[priority, projectId, sessionEpoch, url],
|
|
145
157
|
);
|
|
146
158
|
const snapshot = useThumbnailLease(request);
|
|
@@ -182,7 +194,7 @@ export const CompositionThumbnail = memo(function CompositionThumbnail({
|
|
|
182
194
|
</div>
|
|
183
195
|
)}
|
|
184
196
|
{snapshot.status === "loading" && (
|
|
185
|
-
<div className="absolute inset-0 animate-pulse bg-
|
|
197
|
+
<div className="absolute inset-0 animate-pulse bg-text-0/[0.035]" />
|
|
186
198
|
)}
|
|
187
199
|
{label && (
|
|
188
200
|
<div className="absolute inset-y-0 left-3 z-10 flex items-center">
|
|
@@ -147,7 +147,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
|
|
|
147
147
|
<div className="px-4 py-2 border-b border-neutral-800/40 max-h-24 overflow-y-auto">
|
|
148
148
|
{elementsInRange.map((el) => (
|
|
149
149
|
<div key={el.id} className="flex items-center justify-between py-0.5">
|
|
150
|
-
<span className="text-[10px] font-mono text-
|
|
150
|
+
<span className="text-[10px] font-mono text-accent-ink/80">#{el.id}</span>
|
|
151
151
|
<span className="text-[10px] text-neutral-600">{el.tag}</span>
|
|
152
152
|
</div>
|
|
153
153
|
))}
|
|
@@ -174,7 +174,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
|
|
|
174
174
|
|
|
175
175
|
{/* Action */}
|
|
176
176
|
{copyError && (
|
|
177
|
-
<p className="px-3 pb-2 text-[10px] text-
|
|
177
|
+
<p className="px-3 pb-2 text-[10px] text-danger-ink" role="alert">
|
|
178
178
|
Copy failed — check clipboard permissions and try again.
|
|
179
179
|
</p>
|
|
180
180
|
)}
|
|
@@ -184,7 +184,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
|
|
|
184
184
|
disabled={!buildPromptCopyText(prompt)}
|
|
185
185
|
className={`py-1.5 text-[11px] font-medium rounded-lg transition-all border ${
|
|
186
186
|
copiedPromptOnly
|
|
187
|
-
? "bg-
|
|
187
|
+
? "bg-accent/15 text-accent-ink border-accent/30"
|
|
188
188
|
: "bg-neutral-800/70 text-neutral-200 border-neutral-700/50 hover:bg-neutral-800"
|
|
189
189
|
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
190
190
|
>
|
|
@@ -194,13 +194,13 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
|
|
|
194
194
|
onClick={handleCopy}
|
|
195
195
|
className={`py-1.5 text-[11px] font-medium rounded-lg transition-all ${
|
|
196
196
|
copiedAgentPrompt
|
|
197
|
-
? "bg-
|
|
198
|
-
: "bg-studio-accent/15 text-
|
|
197
|
+
? "bg-accent/15 text-accent-ink border border-accent/30"
|
|
198
|
+
: "bg-studio-accent/15 text-accent-ink border border-studio-accent/25 hover:bg-studio-accent/25"
|
|
199
199
|
}`}
|
|
200
200
|
>
|
|
201
201
|
{copiedAgentPrompt ? "Copied!" : "Copy to Agent"}
|
|
202
202
|
{!copiedAgentPrompt && (
|
|
203
|
-
<span className="text-[9px] text-
|
|
203
|
+
<span className="text-[9px] text-text-muted ml-1.5">Cmd+Enter</span>
|
|
204
204
|
)}
|
|
205
205
|
</button>
|
|
206
206
|
</div>
|
|
@@ -42,7 +42,7 @@ interface KeyframeDiamondContextMenuProps {
|
|
|
42
42
|
const ITEM_CLS =
|
|
43
43
|
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-neutral-200 hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left";
|
|
44
44
|
const DESTRUCTIVE_ITEM_CLS =
|
|
45
|
-
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-
|
|
45
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs text-danger-ink hover:bg-neutral-800 focus-visible:bg-neutral-800 outline-hidden cursor-pointer text-left";
|
|
46
46
|
|
|
47
47
|
export function KeyframeDiamondContextMenu({
|
|
48
48
|
state,
|
|
@@ -172,7 +172,7 @@ export function KeyframeDiamondContextMenu({
|
|
|
172
172
|
<button
|
|
173
173
|
type="button"
|
|
174
174
|
role="menuitem"
|
|
175
|
-
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-
|
|
175
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-danger-ink hover:bg-danger/25 focus-visible:bg-danger/25 outline-hidden cursor-pointer text-left"
|
|
176
176
|
onClick={() => {
|
|
177
177
|
onDeleteAll(state.element, state.animationId);
|
|
178
178
|
onClose();
|
|
@@ -76,7 +76,7 @@ const LoopButton = memo(function LoopButton({
|
|
|
76
76
|
}}
|
|
77
77
|
disabled={disabled}
|
|
78
78
|
className={`flex h-7 w-7 items-center justify-center rounded-md transition-colors disabled:opacity-30 ${
|
|
79
|
-
loopEnabled ? "text-
|
|
79
|
+
loopEnabled ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200"
|
|
80
80
|
}`}
|
|
81
81
|
aria-label={loopEnabled ? "Disable loop playback" : "Enable loop playback"}
|
|
82
82
|
aria-pressed={loopEnabled}
|
|
@@ -118,7 +118,7 @@ const FullscreenButton = memo(function FullscreenButton({
|
|
|
118
118
|
onToggleFullscreen();
|
|
119
119
|
}}
|
|
120
120
|
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
|
|
121
|
-
isFullscreen ? "text-
|
|
121
|
+
isFullscreen ? "text-accent-ink" : "text-neutral-500 hover:text-neutral-200"
|
|
122
122
|
}`}
|
|
123
123
|
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
124
124
|
>
|
|
@@ -241,7 +241,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
241
241
|
<span ref={timeDisplayRef}>{formatTime(0)}</span>
|
|
242
242
|
{timeDisplayMode === "time" ? (
|
|
243
243
|
<>
|
|
244
|
-
<span className="mx-0.5 text-
|
|
244
|
+
<span className="mx-0.5 text-text-off">/</span>
|
|
245
245
|
<span className="text-neutral-600">{formatTime(duration)}</span>
|
|
246
246
|
</>
|
|
247
247
|
) : null}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useId, useRef, memo } from "react";
|
|
2
2
|
import { formatTime, frameToSeconds } from "../lib/time";
|
|
3
3
|
import { Tooltip } from "../../components/ui";
|
|
4
|
+
import { flatActive, flatIdle } from "../../components/timelineToolbarStyles";
|
|
4
5
|
import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
|
|
5
6
|
import { usePlayerStore } from "../store/playerStore";
|
|
6
7
|
import { DEFAULT_SHORTCUT_SECTIONS, type ShortcutSection } from "./studioShortcuts";
|
|
@@ -77,9 +78,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
77
78
|
ref={triggerRef}
|
|
78
79
|
type="button"
|
|
79
80
|
onClick={() => setShowShortcuts((v) => !v)}
|
|
80
|
-
className={
|
|
81
|
-
showShortcuts ? "text-neutral-200" : "text-neutral-600 hover:text-neutral-300"
|
|
82
|
-
}`}
|
|
81
|
+
className={showShortcuts ? flatActive : flatIdle}
|
|
83
82
|
aria-label="Shortcuts and tools"
|
|
84
83
|
aria-expanded={showShortcuts}
|
|
85
84
|
aria-controls={shortcutsPanelId}
|
|
@@ -110,10 +109,8 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
110
109
|
// Deliberately NOT aria-modal. This is a non-modal disclosure: focus is
|
|
111
110
|
// not trapped and the rest of the editor stays operable, so claiming
|
|
112
111
|
// modality would make assistive tech treat the whole app as inert.
|
|
113
|
-
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-
|
|
112
|
+
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg border border-border bg-raised shadow-popover min-w-[220px] overflow-y-auto outline-hidden"
|
|
114
113
|
style={{
|
|
115
|
-
background: "#161618",
|
|
116
|
-
border: "1px solid rgba(255,255,255,0.08)",
|
|
117
114
|
maxHeight: "min(280px, calc(100vh - 80px))",
|
|
118
115
|
}}
|
|
119
116
|
>
|
|
@@ -145,7 +142,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
145
142
|
</Tooltip>
|
|
146
143
|
</form>
|
|
147
144
|
</div>
|
|
148
|
-
<div
|
|
145
|
+
<div className="border-t border-border" />
|
|
149
146
|
<div className="px-3 pt-2.5 pb-2">
|
|
150
147
|
<p className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider mb-1.5">
|
|
151
148
|
Work area
|
|
@@ -153,10 +150,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
153
150
|
<div className="flex flex-col gap-1">
|
|
154
151
|
<div className="flex items-center justify-between gap-2">
|
|
155
152
|
<div className="flex items-center gap-2">
|
|
156
|
-
<span
|
|
157
|
-
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
158
|
-
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
159
|
-
>
|
|
153
|
+
<span className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center bg-hover">
|
|
160
154
|
I
|
|
161
155
|
</span>
|
|
162
156
|
<span className="text-[10px] text-neutral-400">In-point</span>
|
|
@@ -194,10 +188,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
194
188
|
</div>
|
|
195
189
|
<div className="flex items-center justify-between gap-2">
|
|
196
190
|
<div className="flex items-center gap-2">
|
|
197
|
-
<span
|
|
198
|
-
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
|
|
199
|
-
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
200
|
-
>
|
|
191
|
+
<span className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center bg-hover">
|
|
201
192
|
O
|
|
202
193
|
</span>
|
|
203
194
|
<span className="text-[10px] text-neutral-400">Out-point</span>
|
|
@@ -235,7 +226,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
235
226
|
</div>
|
|
236
227
|
</div>
|
|
237
228
|
</div>
|
|
238
|
-
<div
|
|
229
|
+
<div className="border-t border-border" />
|
|
239
230
|
<div className="px-3 pt-2.5 pb-3 flex flex-col gap-3">
|
|
240
231
|
{sections.map((section, sectionIndex) => (
|
|
241
232
|
<div key={sectionIndex}>
|
|
@@ -245,10 +236,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
245
236
|
<div className="flex flex-col gap-1">
|
|
246
237
|
{section.hints.map((hint, hintIndex) => (
|
|
247
238
|
<div key={hintIndex} className="flex items-center gap-3">
|
|
248
|
-
<span
|
|
249
|
-
className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
|
|
250
|
-
style={{ background: "rgba(255,255,255,0.05)" }}
|
|
251
|
-
>
|
|
239
|
+
<span className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center bg-hover">
|
|
252
240
|
{hint.key}
|
|
253
241
|
</span>
|
|
254
242
|
<span className="text-[10px] text-neutral-400">{hint.label}</span>
|
|
@@ -83,7 +83,7 @@ export const TimelineCanvas = memo(function TimelineCanvas() {
|
|
|
83
83
|
data-active={ghostLaneActive || undefined}
|
|
84
84
|
className={`pointer-events-none absolute flex items-center justify-center rounded-lg border border-dashed text-[11px] transition-colors duration-150 ${
|
|
85
85
|
ghostLaneActive
|
|
86
|
-
? "border-studio-accent/60 bg-studio-accent/6 text-
|
|
86
|
+
? "border-studio-accent/60 bg-studio-accent/6 text-accent-ink"
|
|
87
87
|
: "border-neutral-700/50 text-neutral-500"
|
|
88
88
|
}`}
|
|
89
89
|
style={{
|
|
@@ -5,6 +5,7 @@ import { createRoot } from "react-dom/client";
|
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import type { TimelineElement } from "../store/playerStore";
|
|
7
7
|
import { TimelineClip } from "./TimelineClip";
|
|
8
|
+
import { ClipFadesContext } from "./TimelineClipFades";
|
|
8
9
|
import type { TimelineEditCapabilities } from "./timelineEditing";
|
|
9
10
|
import { defaultTimelineTheme, type TimelineTheme } from "./timelineTheme";
|
|
10
11
|
|
|
@@ -67,6 +68,43 @@ function renderClip({
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
describe("TimelineClip", () => {
|
|
71
|
+
it("hands its content the clip's fades, and none to a clip the mixer does not hear", () => {
|
|
72
|
+
const seen: unknown[] = [];
|
|
73
|
+
function Probe() {
|
|
74
|
+
seen.push(React.useContext(ClipFadesContext));
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const render = (element: TimelineElement) => {
|
|
78
|
+
const host = document.createElement("div");
|
|
79
|
+
document.body.append(host);
|
|
80
|
+
const root = createRoot(host);
|
|
81
|
+
act(() => {
|
|
82
|
+
root.render(
|
|
83
|
+
<TimelineClip
|
|
84
|
+
el={element}
|
|
85
|
+
pps={100}
|
|
86
|
+
clipY={0}
|
|
87
|
+
isSelected={false}
|
|
88
|
+
isHovered={false}
|
|
89
|
+
hasCustomContent
|
|
90
|
+
capabilities={capabilities}
|
|
91
|
+
isComposition={false}
|
|
92
|
+
onHoverStart={vi.fn()}
|
|
93
|
+
onHoverEnd={vi.fn()}
|
|
94
|
+
onClick={vi.fn()}
|
|
95
|
+
onDoubleClick={vi.fn()}
|
|
96
|
+
>
|
|
97
|
+
<Probe />
|
|
98
|
+
</TimelineClip>,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
act(() => root.unmount());
|
|
102
|
+
};
|
|
103
|
+
render({ id: "music", tag: "audio", start: 0, duration: 4, track: 0, fadeIn: 1, fadeOut: 0.5 });
|
|
104
|
+
render({ id: "title", tag: "div", start: 0, duration: 4, track: 0, fadeIn: 1 });
|
|
105
|
+
expect(seen).toEqual([{ fadeIn: 1, fadeOut: 0.5, duration: 4 }, null]);
|
|
106
|
+
});
|
|
107
|
+
|
|
70
108
|
it("renders the clip label above custom content without showing default timecode", () => {
|
|
71
109
|
const { host, root } = renderClip({
|
|
72
110
|
element: { id: "hero", label: "Hero", tag: "div", start: 1, duration: 1, track: 0 },
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
import type { TimelineEditCapabilities } from "./timelineEditing";
|
|
10
10
|
import { isAudioTimelineElement } from "../../utils/timelineInspector";
|
|
11
11
|
import { timelineClipFocusId } from "./timelineNavigationIdentity";
|
|
12
|
-
import { TimelineClipFades } from "./TimelineClipFades";
|
|
12
|
+
import { ClipFadesContext, TimelineClipFades, useClipFadeDraft } from "./TimelineClipFades";
|
|
13
|
+
import { rendersWaveform } from "./AudioWaveform";
|
|
13
14
|
|
|
14
15
|
interface TimelineClipProps {
|
|
15
16
|
el: TimelineElement;
|
|
@@ -82,6 +83,8 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
82
83
|
"--clip-handle": theme.handleColor,
|
|
83
84
|
} as CSSProperties;
|
|
84
85
|
const isAudioClip = isAudioTimelineElement(el);
|
|
86
|
+
const hasFades = (isAudioClip || Boolean(el.hasAudio)) && !isGestureActor;
|
|
87
|
+
const fade = useClipFadeDraft(el);
|
|
85
88
|
const clipClassName = [
|
|
86
89
|
"timeline-clip",
|
|
87
90
|
"absolute",
|
|
@@ -165,7 +168,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
165
168
|
width: 2,
|
|
166
169
|
borderRadius: 1,
|
|
167
170
|
background: "var(--clip-handle)",
|
|
168
|
-
opacity: handleOpacity
|
|
171
|
+
opacity: handleOpacity,
|
|
169
172
|
}}
|
|
170
173
|
/>
|
|
171
174
|
</div>
|
|
@@ -195,7 +198,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
195
198
|
width: 2,
|
|
196
199
|
borderRadius: 1,
|
|
197
200
|
background: "var(--clip-handle)",
|
|
198
|
-
opacity: handleOpacity
|
|
201
|
+
opacity: handleOpacity,
|
|
199
202
|
}}
|
|
200
203
|
/>
|
|
201
204
|
</div>
|
|
@@ -206,16 +209,21 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
206
209
|
{startLabel}-{endLabel}s
|
|
207
210
|
</span>
|
|
208
211
|
)}
|
|
209
|
-
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
<ClipFadesContext.Provider value={hasFades ? fade.shape : null}>
|
|
213
|
+
{children}
|
|
214
|
+
</ClipFadesContext.Provider>
|
|
215
|
+
{/* Fade handles for anything the mixer hears: audio clips and videos marked
|
|
216
|
+
data-has-audio. They write data-fade-in/out, the timeline half of the
|
|
217
|
+
inspector's Fade rows. */}
|
|
218
|
+
{hasFades && (
|
|
214
219
|
<TimelineClipFades
|
|
215
220
|
el={el}
|
|
216
221
|
pps={pps}
|
|
217
222
|
widthPx={widthPx}
|
|
218
223
|
showHandles={(isHovered || isSelected) && !isDragging}
|
|
224
|
+
focusable={isSelected}
|
|
225
|
+
hasWaveform={rendersWaveform(el)}
|
|
226
|
+
fade={fade}
|
|
219
227
|
/>
|
|
220
228
|
)}
|
|
221
229
|
</button>
|