@hyperframes/studio 0.8.145 → 0.8.147
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-DZ5JrMDg.js → hyperframes-player-_eMxRXF_.js} +3 -3
- package/dist/assets/{index-BqFRhNZa.js → index-6okhHdnH.js} +1 -1
- package/dist/assets/{index-BOs6C0aZ.js → index-CEq9KmY2.js} +1 -1
- package/dist/assets/{index-D8W0a0Z9.js → index-bXzd7tvF.js} +135 -135
- package/dist/{chunk-EEXSYY5F.js → chunk-C5BDT7NG.js} +0 -1
- package/dist/{chunk-TP6D3U37.js → chunk-HZDCMUG4.js} +1 -2
- package/dist/{chunk-PZ5AY32C.js → chunk-MLKGABMK.js} +0 -1
- package/dist/{chunk-NZPJUZKT.js → chunk-PHGBG7OG.js} +3 -4
- package/dist/{chunk-Y2OHHNM2.js → chunk-PVFBY7KR.js} +0 -1
- package/dist/{chunk-6JY3MTIX.js → chunk-T43DM7Q6.js} +2 -3
- package/dist/{domEditingLayers-PTP2ICGM.js → domEditingLayers-SMVA3FEW.js} +3 -4
- package/dist/index.css +0 -1
- package/dist/index.html +1 -1
- package/dist/index.js +106 -107
- 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/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/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/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/RenderQueue.tsx +2 -2
- package/src/components/renders/useRenderQueue.ts +3 -3
- 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/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/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.tsx +1 -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/utils/hmrStableContext.ts +1 -1
- package/src/utils/razorSplitTransaction.ts +0 -2
- package/src/utils/sdkCutover.linkedTiming.test.ts +1 -1
- package/dist/chunk-6JY3MTIX.js.map +0 -1
- package/dist/chunk-EEXSYY5F.js.map +0 -1
- package/dist/chunk-NZPJUZKT.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-TP6D3U37.js.map +0 -1
- package/dist/chunk-Y2OHHNM2.js.map +0 -1
- package/dist/domEditingLayers-PTP2ICGM.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
|
@@ -352,7 +352,7 @@ export function useColorGradingController({
|
|
|
352
352
|
const reverted = confirmedGradingRef.current;
|
|
353
353
|
latestGradingRef.current = reverted;
|
|
354
354
|
setGrading(reverted);
|
|
355
|
-
setRuntimeStatus({ state: "unavailable", message: "Save failed
|
|
355
|
+
setRuntimeStatus({ state: "unavailable", message: "Save failed, changes reverted" });
|
|
356
356
|
};
|
|
357
357
|
// The callback is the primary result signal; the rejection path supports
|
|
358
358
|
// other setAttributeLive implementations.
|
|
@@ -489,7 +489,7 @@ export function HotspotTool({
|
|
|
489
489
|
)}
|
|
490
490
|
{sequences.length === 0 && (
|
|
491
491
|
<p className="text-[10px] text-neutral-500 italic">
|
|
492
|
-
Create a branch in the Branches section first
|
|
492
|
+
Create a branch in the Branches section first. Hotspots jump to a branch.
|
|
493
493
|
</p>
|
|
494
494
|
)}
|
|
495
495
|
<label className="text-[11px] text-neutral-400">Hotspot label</label>
|
|
@@ -508,7 +508,7 @@ export function HotspotTool({
|
|
|
508
508
|
onChange={(e) => setTargetSequenceId(e.target.value)}
|
|
509
509
|
aria-label="Target branch sequence"
|
|
510
510
|
>
|
|
511
|
-
<option value=""
|
|
511
|
+
<option value="">Select branch</option>
|
|
512
512
|
{sequences.map((seq) => (
|
|
513
513
|
<option key={seq.id} value={seq.id}>
|
|
514
514
|
{seq.label}
|
|
@@ -220,7 +220,7 @@ export function VariablesBindElement({
|
|
|
220
220
|
{existingDecl.default !== undefined && (
|
|
221
221
|
<span className="font-mono"> ({String(existingDecl.default)})</span>
|
|
222
222
|
)}
|
|
223
|
-
, not the element's own
|
|
223
|
+
, not the element's own, so binding won't change "{trimmedId}".
|
|
224
224
|
</p>
|
|
225
225
|
)}
|
|
226
226
|
<div className="flex items-center justify-end gap-2">
|
|
@@ -235,7 +235,7 @@ function HandoffFooter({
|
|
|
235
235
|
|
|
236
236
|
const EMPTY_STATE = (
|
|
237
237
|
<p className="text-[10px] leading-relaxed text-neutral-500">
|
|
238
|
-
No variables declared. Variables make parts of this composition dynamic
|
|
238
|
+
No variables declared. Variables make parts of this composition dynamic. Declare them here (or
|
|
239
239
|
in <code className="font-mono">data-composition-variables</code>), read them with{" "}
|
|
240
240
|
<code className="font-mono">getVariables()</code>, and pass values at render time with{" "}
|
|
241
241
|
<code className="font-mono">--variables</code>.
|
|
@@ -437,7 +437,7 @@ export const VariablesPanel = memo(function VariablesPanel({
|
|
|
437
437
|
const wanted = action.declaration(id).type;
|
|
438
438
|
if (existing && existing.type !== wanted) {
|
|
439
439
|
showToast(
|
|
440
|
-
`"${id}" is already a ${existing.type} variable
|
|
440
|
+
`"${id}" is already a ${existing.type} variable. Pick another id for this ${wanted} binding`,
|
|
441
441
|
"error",
|
|
442
442
|
);
|
|
443
443
|
return;
|
|
@@ -508,7 +508,7 @@ export const VariablesPanel = memo(function VariablesPanel({
|
|
|
508
508
|
/>
|
|
509
509
|
{usage?.scanIncomplete && (
|
|
510
510
|
<p className="text-[9px] text-neutral-600">
|
|
511
|
-
Scripts access variables dynamically
|
|
511
|
+
Scripts access variables dynamically, so usage info may be incomplete.
|
|
512
512
|
</p>
|
|
513
513
|
)}
|
|
514
514
|
{declarations.length > 0 && (
|
|
@@ -131,7 +131,7 @@ function scaleOptionLabel(
|
|
|
131
131
|
// Explain *why* an option is disabled instead of greying it silently:
|
|
132
132
|
// the preset must be an exact integer upscale of the authored size.
|
|
133
133
|
if (dims && !scaleApplies(scale, dims)) {
|
|
134
|
-
return `${base}
|
|
134
|
+
return `${base} (not an integer scale of ${dims.width}×${dims.height})`;
|
|
135
135
|
}
|
|
136
136
|
return base;
|
|
137
137
|
}
|
|
@@ -248,7 +248,7 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
|
|
|
248
248
|
.map((f) => (
|
|
249
249
|
<p key={f} className="text-step-9 text-text-4 leading-relaxed">
|
|
250
250
|
<span className="text-text-3 font-medium">{FORMAT_INFO[f].label}</span>
|
|
251
|
-
{"
|
|
251
|
+
{": "}
|
|
252
252
|
{FORMAT_INFO[f].desc}
|
|
253
253
|
</p>
|
|
254
254
|
))}
|
|
@@ -382,7 +382,7 @@ export function useRenderQueue(
|
|
|
382
382
|
try {
|
|
383
383
|
const res = await studioApiFetch(`/api/render/${jobId}/cancel`, { method: "POST" });
|
|
384
384
|
if (!res.ok && res.status !== 404) {
|
|
385
|
-
setActionError("Couldn't cancel on the server
|
|
385
|
+
setActionError("Couldn't cancel on the server. The render may still be running.");
|
|
386
386
|
return;
|
|
387
387
|
}
|
|
388
388
|
// Reconcile with the status the route reports: if the render actually
|
|
@@ -396,7 +396,7 @@ export function useRenderQueue(
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
} catch {
|
|
399
|
-
setActionError("Couldn't reach the server to cancel
|
|
399
|
+
setActionError("Couldn't reach the server to cancel. The render may still be running.");
|
|
400
400
|
}
|
|
401
401
|
},
|
|
402
402
|
[closeActiveEventSource, loadRenders],
|
|
@@ -409,7 +409,7 @@ export function useRenderQueue(
|
|
|
409
409
|
try {
|
|
410
410
|
const res = await studioApiFetch(`/api/render/${jobId}`, { method: "DELETE" });
|
|
411
411
|
if (!res.ok) {
|
|
412
|
-
setActionError("Couldn't delete the render
|
|
412
|
+
setActionError("Couldn't delete the render. It's still on disk.");
|
|
413
413
|
return;
|
|
414
414
|
}
|
|
415
415
|
} catch {
|
|
@@ -209,7 +209,7 @@ export function AssetCard({
|
|
|
209
209
|
draggable
|
|
210
210
|
role="button"
|
|
211
211
|
tabIndex={0}
|
|
212
|
-
aria-label={`${name}
|
|
212
|
+
aria-label={`${name}: open, drag to timeline, right-click for actions`}
|
|
213
213
|
onPointerDown={handlePointerDown}
|
|
214
214
|
onPointerUp={handlePointerUp}
|
|
215
215
|
onKeyDown={(e) => {
|
|
@@ -337,7 +337,7 @@ export function FontRow({
|
|
|
337
337
|
draggable
|
|
338
338
|
role="button"
|
|
339
339
|
tabIndex={0}
|
|
340
|
-
aria-label={`${name}
|
|
340
|
+
aria-label={`${name}: copy path, drag to timeline, right-click for actions`}
|
|
341
341
|
onClick={() => onCopy(asset)}
|
|
342
342
|
onKeyDown={(e) => {
|
|
343
343
|
if (e.target !== e.currentTarget) return;
|
|
@@ -208,7 +208,7 @@ export function AudioRow({
|
|
|
208
208
|
onPointerUp={handlePointerUp}
|
|
209
209
|
role="button"
|
|
210
210
|
tabIndex={0}
|
|
211
|
-
aria-label={`${name}
|
|
211
|
+
aria-label={`${name}: open, drag to timeline, right-click for actions`}
|
|
212
212
|
onKeyDown={(e) => {
|
|
213
213
|
// Only when the row itself is focused — keydowns bubbling from the
|
|
214
214
|
// inner controls (play button) must keep their native activation.
|
|
@@ -197,7 +197,7 @@ function formatCompositionContext(ctx: CompositionContext): string {
|
|
|
197
197
|
`Elements visible at ${formatTime(ctx.currentTime)}:`,
|
|
198
198
|
...visibleNow.map(
|
|
199
199
|
(el) =>
|
|
200
|
-
`- ${el.label || el.id} (track ${el.track}, ${formatTime(el.start)}
|
|
200
|
+
`- ${el.label || el.id} (track ${el.track}, ${formatTime(el.start)} to ${formatTime(el.start + el.duration)}${el.compositionSrc ? `, src: ${el.compositionSrc}` : ""})`,
|
|
201
201
|
),
|
|
202
202
|
);
|
|
203
203
|
}
|
|
@@ -232,7 +232,7 @@ function buildAgentPrompt(
|
|
|
232
232
|
transitions: [
|
|
233
233
|
`Using /hyperframes, add the "${title}" transition (registry: ${name}) between my scenes.`,
|
|
234
234
|
`${description}`,
|
|
235
|
-
`Place this transition at the cut point between the current scene and the next. Set the duration to 0.5
|
|
235
|
+
`Place this transition at the cut point between the current scene and the next. Set the duration to 0.5 to 1s, position it at the scene boundary on the timeline, and make sure the z-index is above both scenes. Adjust colors to match my palette.`,
|
|
236
236
|
].join("\n\n"),
|
|
237
237
|
effects: [
|
|
238
238
|
`Using /hyperframes, add the "${title}" effect (registry: ${name}) as an overlay on my composition.`,
|
|
@@ -380,8 +380,8 @@ function CompCard({
|
|
|
380
380
|
<span className="text-[11px] font-medium text-neutral-300 truncate">{name}</span>
|
|
381
381
|
{isRoot && (
|
|
382
382
|
<span
|
|
383
|
-
aria-label="Root composition
|
|
384
|
-
title="Root composition
|
|
383
|
+
aria-label="Root composition: opens automatically on load"
|
|
384
|
+
title="Root composition: opens automatically on load"
|
|
385
385
|
className="flex-shrink-0 rounded-full bg-neutral-700/60 px-1.5 py-px text-[8px] font-bold uppercase tracking-wide text-neutral-300"
|
|
386
386
|
>
|
|
387
387
|
Root
|
|
@@ -55,7 +55,7 @@ export function PromptPreviewModal({
|
|
|
55
55
|
ref={containerRef}
|
|
56
56
|
role="dialog"
|
|
57
57
|
aria-modal="true"
|
|
58
|
-
aria-label={`Ask agent
|
|
58
|
+
aria-label={`Ask agent: ${title}`}
|
|
59
59
|
tabIndex={-1}
|
|
60
60
|
className="w-[560px] max-h-[80vh] flex flex-col rounded-2xl border border-neutral-800 bg-neutral-950 shadow-2xl outline-hidden"
|
|
61
61
|
onClick={(e) => e.stopPropagation()}
|
|
@@ -181,7 +181,7 @@ export async function createAudioGroupAndAssignMembers({
|
|
|
181
181
|
// then persists `sources: [groupId]` on success, so a quiet no-op leaves the
|
|
182
182
|
// carve aimed at a group that does not exist.
|
|
183
183
|
if (elements.length < 2) {
|
|
184
|
-
throw new Error(`Cannot group ${elements.length} clip(s)
|
|
184
|
+
throw new Error(`Cannot group ${elements.length} clip(s): a group needs at least two`);
|
|
185
185
|
}
|
|
186
186
|
if (!GROUP_ID_PATTERN.test(groupId)) {
|
|
187
187
|
throw new Error(`Invalid audio group id ${JSON.stringify(groupId)}`);
|
|
@@ -236,7 +236,7 @@ export function applyTimelineMoveAttributes(
|
|
|
236
236
|
): string {
|
|
237
237
|
if (!Number.isFinite(start) || !Number.isFinite(duration)) {
|
|
238
238
|
console.warn(
|
|
239
|
-
`[Timeline] applyTimelineMoveAttributes: non-finite timing (start=${start}, duration=${duration})
|
|
239
|
+
`[Timeline] applyTimelineMoveAttributes: non-finite timing (start=${start}, duration=${duration}). Patch skipped`,
|
|
240
240
|
);
|
|
241
241
|
return original;
|
|
242
242
|
}
|
|
@@ -92,7 +92,7 @@ export function useBlockHandlers({
|
|
|
92
92
|
const runBlockInstall = useCallback(
|
|
93
93
|
async <T>(blockName: string, install: () => Promise<T>): Promise<T | null> => {
|
|
94
94
|
if (installingBlockRef.current) {
|
|
95
|
-
showToast("A block is already installing
|
|
95
|
+
showToast("A block is already installing. One moment…", "info");
|
|
96
96
|
return null;
|
|
97
97
|
}
|
|
98
98
|
installingBlockRef.current = true;
|
|
@@ -340,7 +340,7 @@ export function useDomEditSession({
|
|
|
340
340
|
if (members.some((m) => isAudioDomElement(m.element))) {
|
|
341
341
|
showToast(
|
|
342
342
|
members.every((m) => isAudioDomElement(m.element))
|
|
343
|
-
? "Audio clips group into a bus
|
|
343
|
+
? "Audio clips group into a bus. Use FX on the track header"
|
|
344
344
|
: "Can't group audio clips with layout elements",
|
|
345
345
|
"info",
|
|
346
346
|
);
|
|
@@ -93,7 +93,7 @@ export function useEditorSave({
|
|
|
93
93
|
) {
|
|
94
94
|
lastFailureToastAtRef.current = now;
|
|
95
95
|
showToast(
|
|
96
|
-
`Couldn't save ${path}
|
|
96
|
+
`Couldn't save ${path}. Your latest edits are NOT persisted. Check the preview server; editing again retries the save.`,
|
|
97
97
|
"error",
|
|
98
98
|
);
|
|
99
99
|
}
|
|
@@ -95,7 +95,7 @@ export function useFrameCapture({
|
|
|
95
95
|
} catch (fetchErr) {
|
|
96
96
|
clearTimeout(timeout);
|
|
97
97
|
if (fetchErr instanceof DOMException && fetchErr.name === "AbortError") {
|
|
98
|
-
throw new Error("Capture timed out
|
|
98
|
+
throw new Error("Capture timed out: the server took too long to respond");
|
|
99
99
|
}
|
|
100
100
|
throw fetchErr;
|
|
101
101
|
}
|
|
@@ -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(
|
|
@@ -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}
|
|
@@ -298,7 +298,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
298
298
|
if (!lastUnloaded || attempts > 100) {
|
|
299
299
|
if (lastUnloaded && attempts > 100) {
|
|
300
300
|
console.debug(
|
|
301
|
-
"[studio] asset readiness poll hit the 10s cap
|
|
301
|
+
"[studio] asset readiness poll hit the 10s cap, continuing with unloaded assets",
|
|
302
302
|
);
|
|
303
303
|
}
|
|
304
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(
|
|
@@ -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();
|