@hyperframes/studio 0.8.144 → 0.8.146
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-Bw_jIaSX.js → hyperframes-player-tkxyWf5H.js} +3 -3
- package/dist/assets/{index-Cj7W_c7p.js → index-BrdqOkqP.js} +1 -1
- package/dist/assets/{index-BVM9NuWl.js → index-Cg48BxAo.js} +145 -145
- package/dist/assets/{index-DyH9dT7b.js → index-Dllxg4aU.js} +1 -1
- package/dist/{chunk-EEXSYY5F.js → chunk-C5BDT7NG.js} +0 -1
- package/dist/{chunk-PEAKCOUW.js → chunk-HZDCMUG4.js} +2 -100
- package/dist/{chunk-PZ5AY32C.js → chunk-MLKGABMK.js} +0 -1
- package/dist/{chunk-G3EDKRZH.js → chunk-PHGBG7OG.js} +100 -5
- package/dist/{chunk-Y2OHHNM2.js → chunk-PVFBY7KR.js} +0 -1
- package/dist/{chunk-3GHTHPZD.js → chunk-T43DM7Q6.js} +101 -3
- package/dist/{domEditingLayers-PYST4G45.js → domEditingLayers-SMVA3FEW.js} +3 -4
- package/dist/index.css +0 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.html +1 -1
- package/dist/index.js +509 -529
- package/dist/player.js +4 -5
- package/dist/styles/tailwind-preset.js +1 -2
- package/dist/ui.js +3 -4
- package/package.json +8 -8
- package/src/captions/components/CaptionOverlay.tsx +1 -1
- package/src/captions/hooks/useCaptionSync.ts +1 -1
- package/src/components/LinkedSelectionToggle.tsx +2 -2
- package/src/components/LintModal.tsx +1 -5
- package/src/components/MediaPreview.tsx +2 -2
- package/src/components/ProjectUnreachableBanner.tsx +1 -1
- package/src/components/StudioLeftPanels.tsx +2 -2
- package/src/components/StudioRightPanels.tsx +1 -1
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +6 -6
- package/src/components/editor/AnimationCard.tsx +3 -3
- package/src/components/editor/AnimationCardParts.tsx +1 -1
- package/src/components/editor/BlockParamsPanel.tsx +3 -3
- package/src/components/editor/ComputedTweenNotice.tsx +2 -2
- package/src/components/editor/FileTree.tsx +1 -1
- package/src/components/editor/GsapAnimationList.tsx +1 -1
- package/src/components/editor/GsapAnimationSection.tsx +2 -2
- package/src/components/editor/LayersPanel.test.ts +4 -4
- package/src/components/editor/LayersPanel.tsx +2 -2
- package/src/components/editor/OffCanvasIndicators.tsx +1 -1
- package/src/components/editor/PromotableControl.tsx +1 -1
- package/src/components/editor/PropertyPanel.tsx +2 -2
- package/src/components/editor/SnapToolbar.tsx +2 -2
- package/src/components/editor/TopologyLens.test.tsx +5 -4
- package/src/components/editor/Transform3DCube.tsx +1 -1
- package/src/components/editor/gsapAnimationConstants.ts +2 -2
- package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
- package/src/components/editor/gsapAnimationHelpers.ts +1 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
- package/src/components/editor/propertyPanelFill.tsx +1 -1
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +3 -3
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +1 -1
- package/src/components/editor/propertyPanelFlatTextSection.tsx +1 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -3
- package/src/components/editor/propertyPanelFxBandRuler.tsx +2 -2
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +2 -2
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTransformCommit.ts +3 -3
- package/src/components/editor/useColorGradingController.ts +1 -1
- package/src/components/panels/SlideshowSubPanels.tsx +2 -2
- package/src/components/panels/VariablesBindElement.tsx +1 -1
- package/src/components/panels/VariablesPanel.tsx +3 -3
- package/src/components/renders/GpuEncodingToggle.tsx +28 -0
- package/src/components/renders/RenderQueue.test.tsx +20 -2
- package/src/components/renders/RenderQueue.tsx +15 -3
- package/src/components/renders/RenderQueuePanel.tsx +2 -1
- package/src/components/renders/renderSettings.ts +5 -2
- package/src/components/renders/useRenderQueue.ts +6 -3
- package/src/components/renders/useRenderQueueComposition.test.tsx +12 -0
- package/src/components/sidebar/AssetCard.tsx +2 -2
- package/src/components/sidebar/AudioRow.tsx +1 -1
- package/src/components/sidebar/BlocksTab.tsx +2 -2
- package/src/components/sidebar/CompositionsTab.tsx +2 -2
- package/src/components/sidebar/PromptPreviewModal.tsx +1 -1
- package/src/hooks/timelineAudioGroupCreate.ts +1 -1
- package/src/hooks/timelineEditingHelpers.ts +1 -1
- package/src/hooks/useBlockHandlers.ts +1 -1
- package/src/hooks/useConsoleErrorCapture.test.tsx +21 -2
- package/src/hooks/useConsoleErrorCapture.ts +3 -2
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useFrameCapture.ts +1 -1
- package/src/hooks/useGestureCommit.ts +4 -4
- package/src/hooks/useGsapScriptCommits.ts +1 -1
- package/src/index.ts +1 -0
- package/src/player/components/AudioGainDialog.test.tsx +1 -1
- package/src/player/components/AudioGainDialog.tsx +2 -1
- package/src/player/components/EditModal.tsx +2 -2
- package/src/player/components/KeyframeDiamondContextMenu.tsx +1 -1
- package/src/player/components/OutOfSyncBadge.tsx +1 -1
- package/src/player/components/Player.test.ts +44 -0
- package/src/player/components/Player.tsx +12 -1
- package/src/player/components/ShortcutsPanel.tsx +2 -2
- package/src/player/components/Timeline.virtualization.test.tsx +1 -1
- package/src/player/components/TimelineAutomationLaneSlot.tsx +1 -1
- package/src/player/components/TimelineClip.tsx +1 -1
- package/src/player/components/TimelineFxButton.tsx +2 -2
- package/src/player/components/TimelineTrackHeader.test.tsx +3 -3
- package/src/player/components/timelineClipDragCommit.ts +2 -2
- package/src/player/components/trackHeaderLabelRows.tsx +1 -1
- package/src/player/lib/playbackAdapter.ts +1 -1
- package/src/player/lib/playbackTypes.ts +1 -0
- package/src/player/sceneSwap.test.ts +27 -1
- package/src/player/sceneSwap.ts +16 -2
- package/src/utils/hmrStableContext.ts +1 -1
- package/src/utils/razorSplitTransaction.ts +0 -2
- package/src/utils/sdkCutover.linkedTiming.test.ts +1 -1
- package/dist/chunk-3GHTHPZD.js.map +0 -1
- package/dist/chunk-EEXSYY5F.js.map +0 -1
- package/dist/chunk-G3EDKRZH.js.map +0 -1
- package/dist/chunk-PEAKCOUW.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-Y2OHHNM2.js.map +0 -1
- package/dist/domEditingLayers-PYST4G45.js.map +0 -1
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/player.js.map +0 -1
- package/dist/styles/tailwind-preset.js.map +0 -1
- package/dist/ui.js.map +0 -1
|
@@ -182,11 +182,11 @@ export function useGestureCommit({
|
|
|
182
182
|
frozenSamples.length > 0 ? (frozenSamples[frozenSamples.length - 1]?.time ?? 0) : 0;
|
|
183
183
|
|
|
184
184
|
if (frozenSamples.length <= 2) {
|
|
185
|
-
showToast("No gesture detected
|
|
185
|
+
showToast("No gesture detected. Move the pointer while recording", "error");
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
if (duration <= 0) {
|
|
189
|
-
showToast("Recording too short
|
|
189
|
+
showToast("Recording too short. Try again", "error");
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -213,7 +213,7 @@ export function useGestureCommit({
|
|
|
213
213
|
// would record the gesture onto every sibling sharing it.
|
|
214
214
|
const selector = sel.id ? idSelector(sel.id) : sel.selector;
|
|
215
215
|
if (!selector) {
|
|
216
|
-
showToast("Cannot save
|
|
216
|
+
showToast("Cannot save: element has no selector", "error");
|
|
217
217
|
return;
|
|
218
218
|
}
|
|
219
219
|
// A recorded gesture becomes a NEW tween, so its target must address one
|
|
@@ -412,7 +412,7 @@ export function useGestureCommit({
|
|
|
412
412
|
}
|
|
413
413
|
const iframe = previewIframeRef.current;
|
|
414
414
|
if (!iframe) {
|
|
415
|
-
showToast("Preview not ready
|
|
415
|
+
showToast("Preview not ready. Try again", "error");
|
|
416
416
|
return;
|
|
417
417
|
}
|
|
418
418
|
|
|
@@ -81,7 +81,7 @@ async function runMutationRequest(
|
|
|
81
81
|
): Promise<MutationResult | undefined> {
|
|
82
82
|
const unsafeFields = mutations.flatMap((mutation) => findUnsafeMutationValues(mutation));
|
|
83
83
|
if (unsafeFields.length > 0) {
|
|
84
|
-
showToast?.("Couldn't read element layout
|
|
84
|
+
showToast?.("Couldn't read element layout. Try again at a different playhead time", "error");
|
|
85
85
|
if (options.skipReload) return;
|
|
86
86
|
throw markStudioSaveErrorAlreadyToasted(
|
|
87
87
|
new Error(
|
package/src/index.ts
CHANGED
|
@@ -202,6 +202,7 @@ export type { UseClipboardOptions } from "./hooks/useClipboard";
|
|
|
202
202
|
export { useDomEditSession } from "./hooks/useDomEditSession";
|
|
203
203
|
export type { UseDomEditSessionParams } from "./hooks/useDomEditSession";
|
|
204
204
|
export { useLivePreviewIframe } from "./hooks/useLivePreviewIframe";
|
|
205
|
+
export { announcePreviewDocumentLoaded } from "./player/sceneSwap";
|
|
205
206
|
export type { PreviewPromotion } from "./player/sceneSwap";
|
|
206
207
|
export { usePreviewPersistence } from "./hooks/usePreviewPersistence";
|
|
207
208
|
export type { UsePreviewPersistenceParams } from "./hooks/usePreviewPersistence";
|
|
@@ -126,7 +126,7 @@ describe("AudioGainDialog", () => {
|
|
|
126
126
|
it("disables the peak options when the clip cannot be measured", async () => {
|
|
127
127
|
stubPeaks({});
|
|
128
128
|
open([clip("unmeasured")]);
|
|
129
|
-
await vi.waitFor(() => expect(peakText()).toBe("Peak Amplitude:
|
|
129
|
+
await vi.waitFor(() => expect(peakText()).toBe("Peak Amplitude: Unavailable"));
|
|
130
130
|
expect(row("Normalize Max Peak to")?.querySelector("input")?.disabled).toBe(true);
|
|
131
131
|
expect(row("Adjust Gain by")?.querySelector("input")?.disabled).toBe(false);
|
|
132
132
|
});
|
|
@@ -185,7 +185,8 @@ export function AudioGainDialog({
|
|
|
185
185
|
onSelect={() => setMode("loudness")}
|
|
186
186
|
/>
|
|
187
187
|
<div className="mt-3 text-neutral-400" data-testid="audio-gain-peak">
|
|
188
|
-
Peak Amplitude:
|
|
188
|
+
Peak Amplitude:{" "}
|
|
189
|
+
{peaks === null ? "measuring…" : peakDb === null ? "Unavailable" : dbText(peakDb)}
|
|
189
190
|
</div>
|
|
190
191
|
{outOfRange && <div className="mt-1 text-red-400">Peak targets must be 0 dB or lower.</div>}
|
|
191
192
|
<div className="mt-4 flex justify-end gap-2">
|
|
@@ -134,7 +134,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
|
|
|
134
134
|
<div className="flex items-center gap-2">
|
|
135
135
|
<div className="w-1.5 h-1.5 rounded-full bg-studio-accent" />
|
|
136
136
|
<span className="text-[11px] font-medium text-neutral-300">
|
|
137
|
-
{formatTime(start)}
|
|
137
|
+
{formatTime(start)} to {formatTime(end)}
|
|
138
138
|
</span>
|
|
139
139
|
</div>
|
|
140
140
|
<span className="text-[10px] text-neutral-600">
|
|
@@ -175,7 +175,7 @@ export function EditPopover({ rangeStart, rangeEnd, anchorX, anchorY, onClose }:
|
|
|
175
175
|
{/* Action */}
|
|
176
176
|
{copyError && (
|
|
177
177
|
<p className="px-3 pb-2 text-[10px] text-danger-ink" role="alert">
|
|
178
|
-
Copy failed
|
|
178
|
+
Copy failed. Check clipboard permissions and try again.
|
|
179
179
|
</p>
|
|
180
180
|
)}
|
|
181
181
|
<div className="grid grid-cols-2 gap-2 px-3 pb-3">
|
|
@@ -103,7 +103,7 @@ export const OutOfSyncBadge = memo(function OutOfSyncBadge({ el }: { el: Timelin
|
|
|
103
103
|
tabIndex={-1}
|
|
104
104
|
aria-haspopup="menu"
|
|
105
105
|
aria-label={`Out of sync with ${partnerName} by ${label} frames`}
|
|
106
|
-
title={`Out of sync with ${partnerName} by ${label}
|
|
106
|
+
title={`Out of sync with ${partnerName} by ${label}. Click for Move or Slip into Sync`}
|
|
107
107
|
data-testid="out-of-sync-badge"
|
|
108
108
|
className="absolute bottom-0.5 left-1 z-[12] cursor-pointer rounded-[3px] bg-red-600 px-1 font-mono text-[9px] leading-[14px] text-white tabular-nums"
|
|
109
109
|
onPointerDown={stop}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
shouldShowCompositionLoadingOverlay,
|
|
11
11
|
} from "./Player";
|
|
12
12
|
import { usePlayerStore } from "../store/playerStore";
|
|
13
|
+
import { onPreviewDocumentLoaded } from "../sceneSwap";
|
|
13
14
|
|
|
14
15
|
vi.mock("@hyperframes/player", () => ({}));
|
|
15
16
|
|
|
@@ -196,6 +197,49 @@ describe("callbacks after the player is already mounted", () => {
|
|
|
196
197
|
expect(first).not.toHaveBeenCalled();
|
|
197
198
|
});
|
|
198
199
|
|
|
200
|
+
it("loads the preview on the runtime's ready while a stalled font holds the window's load", async () => {
|
|
201
|
+
const onLoad = vi.fn();
|
|
202
|
+
const { player } = await mountPlayer({ onLoad });
|
|
203
|
+
const iframe = (player as TestHyperframesPlayer).iframeElement;
|
|
204
|
+
let page = {};
|
|
205
|
+
Object.defineProperty(iframe, "contentDocument", { get: () => page });
|
|
206
|
+
Object.defineProperty(iframe, "contentWindow", { get: () => ({ __playerReady: true }) });
|
|
207
|
+
const announced = vi.fn();
|
|
208
|
+
onPreviewDocumentLoaded(iframe, announced);
|
|
209
|
+
|
|
210
|
+
act(() => void player.dispatchEvent(new Event("ready")));
|
|
211
|
+
expect(onLoad).toHaveBeenCalledTimes(1);
|
|
212
|
+
|
|
213
|
+
act(() => void iframe.dispatchEvent(new Event("load")));
|
|
214
|
+
act(() => void player.dispatchEvent(new Event("ready")));
|
|
215
|
+
expect(onLoad).toHaveBeenCalledTimes(1);
|
|
216
|
+
|
|
217
|
+
page = {};
|
|
218
|
+
act(() => void player.dispatchEvent(new Event("ready")));
|
|
219
|
+
expect(onLoad).toHaveBeenCalledTimes(2);
|
|
220
|
+
expect(announced).toHaveBeenCalledTimes(2);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("leaves a ready that arrives before the page's runtime boots to the page's own load", async () => {
|
|
224
|
+
const onLoad = vi.fn();
|
|
225
|
+
const { player } = await mountPlayer({ onLoad });
|
|
226
|
+
const iframe = (player as TestHyperframesPlayer).iframeElement;
|
|
227
|
+
const page = {};
|
|
228
|
+
let runtime: { __playerReady?: boolean } = {};
|
|
229
|
+
Object.defineProperty(iframe, "contentDocument", { get: () => page });
|
|
230
|
+
Object.defineProperty(iframe, "contentWindow", { get: () => runtime });
|
|
231
|
+
|
|
232
|
+
act(() => void player.dispatchEvent(new Event("ready")));
|
|
233
|
+
expect(onLoad).not.toHaveBeenCalled();
|
|
234
|
+
|
|
235
|
+
act(() => void iframe.dispatchEvent(new Event("load")));
|
|
236
|
+
expect(onLoad).toHaveBeenCalledTimes(1);
|
|
237
|
+
|
|
238
|
+
runtime = { __playerReady: true };
|
|
239
|
+
act(() => void player.dispatchEvent(new Event("ready")));
|
|
240
|
+
expect(onLoad).toHaveBeenCalledTimes(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
199
243
|
it("reports the preview error cause", async () => {
|
|
200
244
|
const onPreviewError = vi.fn();
|
|
201
245
|
const { player } = await mountPlayer({ onPreviewError });
|
|
@@ -5,6 +5,8 @@ import { useMountEffect } from "../../hooks/useMountEffect";
|
|
|
5
5
|
import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
|
|
6
6
|
import { HyperframesLoader } from "../../components/ui";
|
|
7
7
|
import { usePlayerStore } from "../store/playerStore";
|
|
8
|
+
import { announcePreviewDocumentLoaded } from "../sceneSwap";
|
|
9
|
+
import type { IframeWindow } from "../lib/playbackTypes";
|
|
8
10
|
// Importing "@hyperframes/player" registers a class extending HTMLElement at
|
|
9
11
|
// module load, which throws under SSR, hence the dynamic import behind a
|
|
10
12
|
// `typeof window` guard. Kicking it here rather than in the mount effect puts
|
|
@@ -128,6 +130,9 @@ export function hasUnloadedAssets(iframe: HTMLIFrameElement, lastResult: boolean
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
const runtimeBootedIn = (iframe: HTMLIFrameElement) =>
|
|
134
|
+
(iframe.contentWindow as IframeWindow | null)?.__playerReady === true;
|
|
135
|
+
|
|
131
136
|
/**
|
|
132
137
|
* Renders a composition preview using the <hyperframes-player> web component.
|
|
133
138
|
*
|
|
@@ -228,9 +233,11 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
228
233
|
const loading = getShaderTransitionLoading(event);
|
|
229
234
|
if (loading !== null) setShaderTransitionLoading(loading);
|
|
230
235
|
};
|
|
236
|
+
let loadedDocument: Document | null = null;
|
|
231
237
|
const handleReady = () => {
|
|
232
238
|
setPreviewError(null);
|
|
233
239
|
setCompositionLoading(false);
|
|
240
|
+
if (iframe.contentDocument && runtimeBootedIn(iframe)) handleLoad();
|
|
234
241
|
};
|
|
235
242
|
const handlePainted = () => {
|
|
236
243
|
setPainted(true);
|
|
@@ -243,6 +250,9 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
243
250
|
setCompositionLoading(false);
|
|
244
251
|
};
|
|
245
252
|
const handleLoad = () => {
|
|
253
|
+
const doc = iframe.contentDocument;
|
|
254
|
+
if (doc && doc === loadedDocument) return;
|
|
255
|
+
loadedDocument = doc;
|
|
246
256
|
loadCountRef.current++;
|
|
247
257
|
setLoaded(true);
|
|
248
258
|
setPreviewError(null);
|
|
@@ -258,6 +268,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
258
268
|
container.addEventListener("animationend", onEnd, { once: true });
|
|
259
269
|
}
|
|
260
270
|
onLoadRef.current();
|
|
271
|
+
announcePreviewDocumentLoaded(iframe);
|
|
261
272
|
|
|
262
273
|
// Show a loading overlay until every `<video>`/`<audio>` and Lottie
|
|
263
274
|
// asset is ready. Without this users can click play before audio has
|
|
@@ -287,7 +298,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
287
298
|
if (!lastUnloaded || attempts > 100) {
|
|
288
299
|
if (lastUnloaded && attempts > 100) {
|
|
289
300
|
console.debug(
|
|
290
|
-
"[studio] asset readiness poll hit the 10s cap
|
|
301
|
+
"[studio] asset readiness poll hit the 10s cap, continuing with unloaded assets",
|
|
291
302
|
);
|
|
292
303
|
}
|
|
293
304
|
if (assetPollRef.current) clearInterval(assetPollRef.current);
|
|
@@ -182,7 +182,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
182
182
|
</Tooltip>
|
|
183
183
|
</>
|
|
184
184
|
) : (
|
|
185
|
-
<span className="text-[10px] text-neutral-600"
|
|
185
|
+
<span className="text-[10px] text-neutral-600">Not set</span>
|
|
186
186
|
)}
|
|
187
187
|
</div>
|
|
188
188
|
</div>
|
|
@@ -220,7 +220,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
|
|
|
220
220
|
</Tooltip>
|
|
221
221
|
</>
|
|
222
222
|
) : (
|
|
223
|
-
<span className="text-[10px] text-neutral-600"
|
|
223
|
+
<span className="text-[10px] text-neutral-600">Not set</span>
|
|
224
224
|
)}
|
|
225
225
|
</div>
|
|
226
226
|
</div>
|
|
@@ -167,7 +167,7 @@ describe("Timeline row virtualization", { timeout: 30_000 }, () => {
|
|
|
167
167
|
const clip = host.querySelector<HTMLElement>('[data-el-id="clip-0"]');
|
|
168
168
|
expect(scroller).not.toBeNull();
|
|
169
169
|
expect(clip).not.toBeNull();
|
|
170
|
-
expect(clip?.title).toBe("Clip 0 • 0.0s
|
|
170
|
+
expect(clip?.title).toBe("Clip 0 • 0.0s to 10.0s");
|
|
171
171
|
const picture = host.querySelector("[data-rich-content]");
|
|
172
172
|
expect(picture).not.toBeNull();
|
|
173
173
|
|
|
@@ -110,7 +110,7 @@ function ClipAutomationLanes({
|
|
|
110
110
|
// — because "not editable" without that reads as broken.
|
|
111
111
|
readOnlyNote={
|
|
112
112
|
isCarveLane(lane.target, bound.chain)
|
|
113
|
-
? "Owned by the voiceover carve
|
|
113
|
+
? "Owned by the voiceover carve and re-derived on every analysis. Change strength in the FX rack, or turn the carve off to edit these by hand."
|
|
114
114
|
: bound.readOnly
|
|
115
115
|
? "Read-only here."
|
|
116
116
|
: undefined
|
|
@@ -170,7 +170,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
170
170
|
title={
|
|
171
171
|
isComposition
|
|
172
172
|
? `${el.compositionSrc} • Double-click to open`
|
|
173
|
-
: `${displayLabel} • ${startLabel}s
|
|
173
|
+
: `${displayLabel} • ${startLabel}s to ${endLabel}s`
|
|
174
174
|
}
|
|
175
175
|
onPointerEnter={onHoverStart}
|
|
176
176
|
onPointerLeave={onHoverEnd}
|
|
@@ -213,7 +213,7 @@ export function TimelineFxButton(props: TimelineFxButtonProps) {
|
|
|
213
213
|
type="button"
|
|
214
214
|
tabIndex={-1}
|
|
215
215
|
ref={buttonRef}
|
|
216
|
-
aria-label="Effects
|
|
216
|
+
aria-label="Effects: group these clips first"
|
|
217
217
|
title="Group these clips to add effects to all of them"
|
|
218
218
|
className="flex h-6 items-center justify-center rounded-sm border-0 bg-transparent px-1 text-[10px] font-semibold text-[var(--timeline-text-faint)] hover:text-[var(--timeline-text-soft)]"
|
|
219
219
|
onPointerDown={(event) => event.stopPropagation()}
|
|
@@ -253,7 +253,7 @@ export function TimelineFxButton(props: TimelineFxButtonProps) {
|
|
|
253
253
|
type="button"
|
|
254
254
|
tabIndex={-1}
|
|
255
255
|
ref={buttonRef}
|
|
256
|
-
aria-label={nodeCount > 0 ? `Effects
|
|
256
|
+
aria-label={nodeCount > 0 ? `Effects: ${nodeCount} applied` : "Effects"}
|
|
257
257
|
title="Effects"
|
|
258
258
|
className={`flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[10px] font-semibold transition-colors ${
|
|
259
259
|
open || nodeCount > 0
|
|
@@ -851,7 +851,7 @@ describe("TimelineTrackHeader", () => {
|
|
|
851
851
|
isAudioTrack: true,
|
|
852
852
|
};
|
|
853
853
|
const pointer = (host: HTMLElement) =>
|
|
854
|
-
host.querySelector('button[aria-label="Effects
|
|
854
|
+
host.querySelector('button[aria-label="Effects: group these clips first"]');
|
|
855
855
|
const view = renderHeader(opts);
|
|
856
856
|
expect(pointer(view.host)).not.toBeNull();
|
|
857
857
|
// One clip needs no grouping — the real FX button takes its place.
|
|
@@ -886,7 +886,7 @@ describe("TimelineTrackHeader", () => {
|
|
|
886
886
|
};
|
|
887
887
|
const pointer = (host: HTMLElement) =>
|
|
888
888
|
host.querySelector<HTMLButtonElement>(
|
|
889
|
-
'button[aria-label="Effects
|
|
889
|
+
'button[aria-label="Effects: group these clips first"]',
|
|
890
890
|
);
|
|
891
891
|
const view = renderHeader(opts);
|
|
892
892
|
const button = pointer(view.host);
|
|
@@ -955,7 +955,7 @@ describe("TimelineTrackHeader", () => {
|
|
|
955
955
|
const controls = line?.lastElementChild as HTMLElement | null;
|
|
956
956
|
expect(controls?.className).toContain("ml-auto");
|
|
957
957
|
expect(
|
|
958
|
-
controls?.querySelector('button[aria-label="Effects
|
|
958
|
+
controls?.querySelector('button[aria-label="Effects: group these clips first"]'),
|
|
959
959
|
).not.toBeNull();
|
|
960
960
|
// And the clip count is beside the name, not out with the controls.
|
|
961
961
|
expect(controls?.querySelector('[aria-label="2 clips"]')).toBeNull();
|
|
@@ -130,8 +130,8 @@ export function persistMoveEdits(
|
|
|
130
130
|
if (!onMoveElements) {
|
|
131
131
|
console.warn(
|
|
132
132
|
onMoveElement
|
|
133
|
-
? `[Timeline] persistMoveEdits: only single-clip onMoveElement wired
|
|
134
|
-
: `[Timeline] persistMoveEdits: no move persist handler wired
|
|
133
|
+
? `[Timeline] persistMoveEdits: only single-clip onMoveElement wired, so this ${edits.length}-clip move degrades to a per-clip persist race (no atomic single-undo)`
|
|
134
|
+
: `[Timeline] persistMoveEdits: no move persist handler wired, so ${edits.length} edit(s) applied to the store only, not saved`,
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
const prev = edits.map((e) => ({
|
|
@@ -286,7 +286,7 @@ export function AutomationLaneHeaderRow({
|
|
|
286
286
|
<span
|
|
287
287
|
data-automation-lane-also=""
|
|
288
288
|
className="truncate text-[9px] text-warning-ink"
|
|
289
|
-
title={`${alsoAutomatedBy} is also fading this
|
|
289
|
+
title={`${alsoAutomatedBy} is also fading this. The two multiply.`}
|
|
290
290
|
>
|
|
291
291
|
{alsoAutomatedBy} is also fading this.
|
|
292
292
|
</span>
|
|
@@ -183,7 +183,7 @@ export function resolveStaticSeekFallback(opts: {
|
|
|
183
183
|
if (!warned.current) {
|
|
184
184
|
warned.current = true;
|
|
185
185
|
console.warn(
|
|
186
|
-
`[useTimelinePlayer] Selected adapter duration (${getAdapterDuration(bestAdapter)}s) does not cover the document duration (${docDuration}s); falling back to seek-driven playback, which never starts media elements or WebAudio. Audio will not play in preview
|
|
186
|
+
`[useTimelinePlayer] Selected adapter duration (${getAdapterDuration(bestAdapter)}s) does not cover the document duration (${docDuration}s); falling back to seek-driven playback, which never starts media elements or WebAudio. Audio will not play in preview. Extend the GSAP timeline to cover the declared data-duration.`,
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
const adapter = createStaticSeekPlaybackAdapter(
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SCENE_SWAP_MS,
|
|
5
|
+
announcePreviewDocumentLoaded,
|
|
6
|
+
onPreviewContentReplaced,
|
|
7
|
+
sceneSwapFor,
|
|
8
|
+
} from "./sceneSwap";
|
|
4
9
|
|
|
5
10
|
afterEach(() => {
|
|
6
11
|
vi.useRealTimers();
|
|
@@ -93,3 +98,24 @@ describe("sceneSwapFor", () => {
|
|
|
93
98
|
expect(swap).toHaveBeenCalledWith("<html></html>", expect.anything());
|
|
94
99
|
});
|
|
95
100
|
});
|
|
101
|
+
|
|
102
|
+
describe("onPreviewContentReplaced", () => {
|
|
103
|
+
it("reports Player's document-loaded announcement and a scene swap, never a raw load", () => {
|
|
104
|
+
const iframe = document.createElement("iframe");
|
|
105
|
+
const replaced = vi.fn();
|
|
106
|
+
const stop = onPreviewContentReplaced(iframe, replaced);
|
|
107
|
+
|
|
108
|
+
iframe.dispatchEvent(new Event("load"));
|
|
109
|
+
expect(replaced).not.toHaveBeenCalled();
|
|
110
|
+
|
|
111
|
+
announcePreviewDocumentLoaded(iframe);
|
|
112
|
+
expect(replaced).toHaveBeenCalledTimes(1);
|
|
113
|
+
|
|
114
|
+
iframe.dispatchEvent(new Event("hf-scenes-swapped"));
|
|
115
|
+
expect(replaced).toHaveBeenCalledTimes(2);
|
|
116
|
+
|
|
117
|
+
stop();
|
|
118
|
+
announcePreviewDocumentLoaded(iframe);
|
|
119
|
+
expect(replaced).toHaveBeenCalledTimes(2);
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/player/sceneSwap.ts
CHANGED
|
@@ -6,14 +6,28 @@ type SwapWindow = Window & {
|
|
|
6
6
|
const SCENES_SWAPPED = "hf-scenes-swapped";
|
|
7
7
|
export const SCENE_SWAP_MS = 5000;
|
|
8
8
|
|
|
9
|
+
const PREVIEW_DOCUMENT_LOADED = "hf-preview-document-loaded";
|
|
10
|
+
|
|
11
|
+
export function announcePreviewDocumentLoaded(iframe: HTMLIFrameElement): void {
|
|
12
|
+
iframe.dispatchEvent(new Event(PREVIEW_DOCUMENT_LOADED));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function onPreviewDocumentLoaded(
|
|
16
|
+
iframe: HTMLIFrameElement,
|
|
17
|
+
onLoaded: () => void,
|
|
18
|
+
): () => void {
|
|
19
|
+
iframe.addEventListener(PREVIEW_DOCUMENT_LOADED, onLoaded);
|
|
20
|
+
return () => iframe.removeEventListener(PREVIEW_DOCUMENT_LOADED, onLoaded);
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
export function onPreviewContentReplaced(
|
|
10
24
|
iframe: HTMLIFrameElement,
|
|
11
25
|
onReplaced: () => void,
|
|
12
26
|
): () => void {
|
|
13
|
-
iframe
|
|
27
|
+
const stopLoaded = onPreviewDocumentLoaded(iframe, onReplaced);
|
|
14
28
|
iframe.addEventListener(SCENES_SWAPPED, onReplaced);
|
|
15
29
|
return () => {
|
|
16
|
-
|
|
30
|
+
stopLoaded();
|
|
17
31
|
iframe.removeEventListener(SCENES_SWAPPED, onReplaced);
|
|
18
32
|
};
|
|
19
33
|
}
|
|
@@ -51,7 +51,7 @@ export function createStableContext<T>(name: string, defaultValue: T): Context<T
|
|
|
51
51
|
// Not on the HMR path: a re-evaluated module hands back the SAME default it
|
|
52
52
|
// registered, which is how a hot reload keeps its context alive.
|
|
53
53
|
console.warn(
|
|
54
|
-
`[hmrStableContext] "${name}" was registered twice with different defaults
|
|
54
|
+
`[hmrStableContext] "${name}" was registered twice with different defaults: ` +
|
|
55
55
|
"two contexts are sharing one identity. Use module path + export name.",
|
|
56
56
|
);
|
|
57
57
|
}
|
|
@@ -18,7 +18,6 @@ interface CutTarget {
|
|
|
18
18
|
elementDuration: number;
|
|
19
19
|
playbackStart?: number;
|
|
20
20
|
playbackRate?: number;
|
|
21
|
-
isComposition?: boolean;
|
|
22
21
|
track?: number;
|
|
23
22
|
}
|
|
24
23
|
|
|
@@ -71,7 +70,6 @@ function buildCutTarget(
|
|
|
71
70
|
elementDuration: element.duration,
|
|
72
71
|
...(element.playbackStart != null ? { playbackStart: element.playbackStart } : {}),
|
|
73
72
|
...(element.playbackRate != null ? { playbackRate: element.playbackRate } : {}),
|
|
74
|
-
...(element.kind === "composition" ? { isComposition: true } : {}),
|
|
75
73
|
// Pin both halves to the current track: unstamped, the runtime's positional
|
|
76
74
|
// fallback (parseAuthoredTrack) renumbers the new sibling onto a new row.
|
|
77
75
|
track: resolveElementTrack(element),
|
|
@@ -43,7 +43,7 @@ describe("a linked trim the SDK refuses", () => {
|
|
|
43
43
|
thrown = error;
|
|
44
44
|
}
|
|
45
45
|
expect(getStudioSaveErrorMessage(thrown)).toBe(
|
|
46
|
-
"Linked audio would start after the new end
|
|
46
|
+
"Linked audio would start after the new end. Unlink or trim the audio first.",
|
|
47
47
|
);
|
|
48
48
|
expect(deps.writeProjectFile).not.toHaveBeenCalled();
|
|
49
49
|
expect(deps.editHistory.recordEdit).not.toHaveBeenCalled();
|