@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.
Files changed (102) hide show
  1. package/dist/assets/{hyperframes-player-DZ5JrMDg.js → hyperframes-player-_eMxRXF_.js} +3 -3
  2. package/dist/assets/{index-BqFRhNZa.js → index-6okhHdnH.js} +1 -1
  3. package/dist/assets/{index-BOs6C0aZ.js → index-CEq9KmY2.js} +1 -1
  4. package/dist/assets/{index-D8W0a0Z9.js → index-bXzd7tvF.js} +135 -135
  5. package/dist/{chunk-EEXSYY5F.js → chunk-C5BDT7NG.js} +0 -1
  6. package/dist/{chunk-TP6D3U37.js → chunk-HZDCMUG4.js} +1 -2
  7. package/dist/{chunk-PZ5AY32C.js → chunk-MLKGABMK.js} +0 -1
  8. package/dist/{chunk-NZPJUZKT.js → chunk-PHGBG7OG.js} +3 -4
  9. package/dist/{chunk-Y2OHHNM2.js → chunk-PVFBY7KR.js} +0 -1
  10. package/dist/{chunk-6JY3MTIX.js → chunk-T43DM7Q6.js} +2 -3
  11. package/dist/{domEditingLayers-PTP2ICGM.js → domEditingLayers-SMVA3FEW.js} +3 -4
  12. package/dist/index.css +0 -1
  13. package/dist/index.html +1 -1
  14. package/dist/index.js +106 -107
  15. package/dist/player.js +4 -5
  16. package/dist/styles/tailwind-preset.js +1 -2
  17. package/dist/ui.js +3 -4
  18. package/package.json +8 -8
  19. package/src/captions/components/CaptionOverlay.tsx +1 -1
  20. package/src/captions/hooks/useCaptionSync.ts +1 -1
  21. package/src/components/LinkedSelectionToggle.tsx +2 -2
  22. package/src/components/LintModal.tsx +1 -5
  23. package/src/components/MediaPreview.tsx +2 -2
  24. package/src/components/ProjectUnreachableBanner.tsx +1 -1
  25. package/src/components/StudioRightPanels.tsx +1 -1
  26. package/src/components/TimelineToolbar.test.tsx +2 -2
  27. package/src/components/TimelineToolbar.tsx +6 -6
  28. package/src/components/editor/AnimationCard.tsx +3 -3
  29. package/src/components/editor/AnimationCardParts.tsx +1 -1
  30. package/src/components/editor/BlockParamsPanel.tsx +3 -3
  31. package/src/components/editor/ComputedTweenNotice.tsx +2 -2
  32. package/src/components/editor/FileTree.tsx +1 -1
  33. package/src/components/editor/GsapAnimationList.tsx +1 -1
  34. package/src/components/editor/GsapAnimationSection.tsx +2 -2
  35. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  36. package/src/components/editor/PromotableControl.tsx +1 -1
  37. package/src/components/editor/PropertyPanel.tsx +2 -2
  38. package/src/components/editor/SnapToolbar.tsx +2 -2
  39. package/src/components/editor/Transform3DCube.tsx +1 -1
  40. package/src/components/editor/gsapAnimationConstants.ts +2 -2
  41. package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
  42. package/src/components/editor/gsapAnimationHelpers.ts +1 -1
  43. package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
  44. package/src/components/editor/propertyPanelFill.tsx +1 -1
  45. package/src/components/editor/propertyPanelFlatMotionSection.tsx +3 -3
  46. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
  47. package/src/components/editor/propertyPanelFlatSelectRow.tsx +1 -1
  48. package/src/components/editor/propertyPanelFlatTextSection.tsx +1 -1
  49. package/src/components/editor/propertyPanelFont.tsx +3 -3
  50. package/src/components/editor/propertyPanelFxBandRuler.tsx +2 -2
  51. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +2 -2
  52. package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
  53. package/src/components/editor/propertyPanelTransformCommit.ts +3 -3
  54. package/src/components/editor/useColorGradingController.ts +1 -1
  55. package/src/components/panels/SlideshowSubPanels.tsx +2 -2
  56. package/src/components/panels/VariablesBindElement.tsx +1 -1
  57. package/src/components/panels/VariablesPanel.tsx +3 -3
  58. package/src/components/renders/RenderQueue.tsx +2 -2
  59. package/src/components/renders/useRenderQueue.ts +3 -3
  60. package/src/components/sidebar/AssetCard.tsx +2 -2
  61. package/src/components/sidebar/AudioRow.tsx +1 -1
  62. package/src/components/sidebar/BlocksTab.tsx +2 -2
  63. package/src/components/sidebar/CompositionsTab.tsx +2 -2
  64. package/src/components/sidebar/PromptPreviewModal.tsx +1 -1
  65. package/src/hooks/timelineAudioGroupCreate.ts +1 -1
  66. package/src/hooks/timelineEditingHelpers.ts +1 -1
  67. package/src/hooks/useBlockHandlers.ts +1 -1
  68. package/src/hooks/useDomEditSession.ts +1 -1
  69. package/src/hooks/useEditorSave.ts +1 -1
  70. package/src/hooks/useFrameCapture.ts +1 -1
  71. package/src/hooks/useGestureCommit.ts +4 -4
  72. package/src/hooks/useGsapScriptCommits.ts +1 -1
  73. package/src/player/components/AudioGainDialog.test.tsx +1 -1
  74. package/src/player/components/AudioGainDialog.tsx +2 -1
  75. package/src/player/components/EditModal.tsx +2 -2
  76. package/src/player/components/KeyframeDiamondContextMenu.tsx +1 -1
  77. package/src/player/components/OutOfSyncBadge.tsx +1 -1
  78. package/src/player/components/Player.tsx +1 -1
  79. package/src/player/components/ShortcutsPanel.tsx +2 -2
  80. package/src/player/components/Timeline.virtualization.test.tsx +1 -1
  81. package/src/player/components/TimelineAutomationLaneSlot.tsx +1 -1
  82. package/src/player/components/TimelineClip.tsx +1 -1
  83. package/src/player/components/TimelineFxButton.tsx +2 -2
  84. package/src/player/components/TimelineTrackHeader.test.tsx +3 -3
  85. package/src/player/components/timelineClipDragCommit.ts +2 -2
  86. package/src/player/components/trackHeaderLabelRows.tsx +1 -1
  87. package/src/player/lib/playbackAdapter.ts +1 -1
  88. package/src/utils/hmrStableContext.ts +1 -1
  89. package/src/utils/razorSplitTransaction.ts +0 -2
  90. package/src/utils/sdkCutover.linkedTiming.test.ts +1 -1
  91. package/dist/chunk-6JY3MTIX.js.map +0 -1
  92. package/dist/chunk-EEXSYY5F.js.map +0 -1
  93. package/dist/chunk-NZPJUZKT.js.map +0 -1
  94. package/dist/chunk-PZ5AY32C.js.map +0 -1
  95. package/dist/chunk-TP6D3U37.js.map +0 -1
  96. package/dist/chunk-Y2OHHNM2.js.map +0 -1
  97. package/dist/domEditingLayers-PTP2ICGM.js.map +0 -1
  98. package/dist/index.css.map +0 -1
  99. package/dist/index.js.map +0 -1
  100. package/dist/player.js.map +0 -1
  101. package/dist/styles/tailwind-preset.js.map +0 -1
  102. 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 — reverted" });
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 — hotspots jump to a branch.
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="">— select branch —</option>
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 — binding won't change "{trimmedId}".
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 — declare them here (or
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 — pick another id for this ${wanted} binding`,
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 — usage info may be incomplete.
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} — not an integer scale of ${dims.width}×${dims.height}`;
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 — the render may still be running.");
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 — the render may still be running.");
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 — it's still on disk.");
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} — open, drag to timeline, right-click for actions`}
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} — copy path, drag to timeline, right-click for actions`}
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} — open, drag to timeline, right-click for actions`}
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)}–${formatTime(el.start + el.duration)}${el.compositionSrc ? `, src: ${el.compositionSrc}` : ""})`,
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–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.`,
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 — opens automatically on load"
384
- title="Root composition — opens automatically on load"
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 — ${title}`}
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) — a group needs at least two`);
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}) — patch skipped`,
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 — one moment…", "info");
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 — use FX on the track header"
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} — your latest edits are NOT persisted. Check the preview server; editing again retries the save.`,
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 — the server took too long to respond");
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 — move the pointer while recording", "error");
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 — try again", "error");
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 — element has no selector", "error");
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 — try again", "error");
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 — try again at a different playhead time", "error");
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: {peaks === null ? "measuring…" : peakDb === null ? "—" : dbText(peakDb)}
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)} — {formatTime(end)}
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 — check clipboard permissions and try again.
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">
@@ -142,7 +142,7 @@ export function KeyframeDiamondContextMenu({
142
142
  {copyStatus === "copied"
143
143
  ? "Copied!"
144
144
  : copyStatus === "failed"
145
- ? "Copy failed — check permissions"
145
+ ? "Copy failed. Check permissions"
146
146
  : "Copy Properties"}
147
147
  </button>
148
148
  )}
@@ -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} — click for Move or Slip into Sync`}
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 — continuing with unloaded assets",
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">—</span>
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">—</span>
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 – 10.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 — re-derived on every analysis. Change strength in the FX rack, or turn the carve off to edit these by hand."
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 – ${endLabel}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 — group these clips first"
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 — ${nodeCount} applied` : "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 — group these clips first"]');
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 — group these clips first"]',
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 — group these clips first"]'),
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 — this ${edits.length}-clip move degrades to a per-clip persist race (no atomic single-undo)`
134
- : `[Timeline] persistMoveEdits: no move persist handler wired — ${edits.length} edit(s) applied to the store only, not saved`,
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 — the two multiply.`}
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 — extend the GSAP timeline to cover the declared data-duration.`,
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 — unlink or trim the audio first.",
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();