hyperframes 0.8.3 → 0.8.5

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.
@@ -24,7 +24,7 @@ import {
24
24
  resolveDomEditSelection,
25
25
  serializeDomEditTextFields,
26
26
  setCompositionSourceMap
27
- } from "./chunk-AZYHQC6V.js";
27
+ } from "./chunk-ZALQ3CW7.js";
28
28
  import {
29
29
  __export
30
30
  } from "./chunk-PZ5AY32C.js";
@@ -21832,12 +21832,14 @@ function buildCompositionThumbnailUrl({
21832
21832
  duration = 5,
21833
21833
  selector,
21834
21834
  selectorIndex,
21835
- origin
21835
+ origin,
21836
+ output
21836
21837
  }) {
21837
21838
  const thumbnailBase = previewUrl.replace("/preview/comp/", "/thumbnail/").replace(/\/preview$/, "/thumbnail/index.html");
21838
21839
  const thumbnailUrl2 = new URL(thumbnailBase, origin);
21839
21840
  thumbnailUrl2.searchParams.set("t", (seekTime + duration / 2).toFixed(2));
21840
21841
  thumbnailUrl2.searchParams.set("v", THUMBNAIL_URL_VERSION);
21842
+ if (output) thumbnailUrl2.searchParams.set("output", output);
21841
21843
  if (selector) {
21842
21844
  thumbnailUrl2.searchParams.set("selector", selector);
21843
21845
  if (selectorIndex != null && selectorIndex > 0) {
@@ -23822,20 +23824,6 @@ var NLEPreview = memo19(function NLEPreview2({
23822
23824
  document.addEventListener("wheel", handleWheel, { passive: false, capture: true });
23823
23825
  return () => document.removeEventListener("wheel", handleWheel, { capture: true });
23824
23826
  }, [applyZoom, applyPan]);
23825
- useEffect27(() => {
23826
- const viewport = viewportRef.current;
23827
- if (!viewport) return;
23828
- const handleDblClick = (event) => {
23829
- if (isPreviewAtFit(zoomRef.current)) return;
23830
- const rect = viewport.getBoundingClientRect();
23831
- if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) {
23832
- return;
23833
- }
23834
- applyZoom(DEFAULT_PREVIEW_ZOOM);
23835
- };
23836
- document.addEventListener("dblclick", handleDblClick, { capture: true });
23837
- return () => document.removeEventListener("dblclick", handleDblClick, { capture: true });
23838
- }, [applyZoom]);
23839
23827
  useEffect27(() => {
23840
23828
  const isInsideViewport = (clientX, clientY) => {
23841
23829
  const viewport = viewportRef.current;
@@ -26425,7 +26413,7 @@ function resolveElementForOverlay(doc, sel, activeCompositionPath, cacheRef) {
26425
26413
 
26426
26414
  // src/components/editor/marqueeCommit.ts
26427
26415
  var MARQUEE_THRESHOLD_PX = 4;
26428
- function collectMarqueeHits(rect, iframe, overlayEl, activeCompositionPath) {
26416
+ function collectMarqueeCandidates(iframe, overlayEl, activeCompositionPath) {
26429
26417
  const doc = iframe.contentDocument;
26430
26418
  if (!doc) return [];
26431
26419
  const root = doc.querySelector("[data-composition-id]") ?? doc.body;
@@ -26438,28 +26426,32 @@ function collectMarqueeHits(rect, iframe, overlayEl, activeCompositionPath) {
26438
26426
  width: declW > 0 ? declW : rootEl.getBoundingClientRect().width || 1,
26439
26427
  height: declH > 0 ? declH : rootEl.getBoundingClientRect().height || 1
26440
26428
  };
26441
- const hits = [];
26429
+ const candidates = [];
26442
26430
  for (const item of items) {
26443
26431
  const el = item.element;
26444
26432
  if (!isElementComputedVisible(el)) continue;
26445
26433
  if (coversComposition(el.getBoundingClientRect(), viewport)) continue;
26446
26434
  const overlayRect = toVisibleOverlayRect(overlayEl, iframe, el);
26447
26435
  if (!overlayRect) continue;
26448
- const r = {
26449
- left: overlayRect.left,
26450
- top: overlayRect.top,
26451
- width: overlayRect.width,
26452
- height: overlayRect.height
26453
- };
26454
- if (!rectsOverlap(rect, r)) continue;
26455
- hits.push({ element: el, rect: r });
26436
+ candidates.push({
26437
+ element: el,
26438
+ rect: {
26439
+ left: overlayRect.left,
26440
+ top: overlayRect.top,
26441
+ width: overlayRect.width,
26442
+ height: overlayRect.height
26443
+ }
26444
+ });
26456
26445
  }
26457
- return hits;
26446
+ return candidates;
26447
+ }
26448
+ function hitsWithin(rect, candidates) {
26449
+ return candidates.filter((candidate) => rectsOverlap(rect, candidate.rect));
26458
26450
  }
26459
- async function runMarqueeIntersection(rect, iframe, overlayEl, activeCompositionPath) {
26451
+ async function runMarqueeIntersection(rect, candidates, activeCompositionPath) {
26460
26452
  const isMasterView = !activeCompositionPath || activeCompositionPath === "index.html";
26461
26453
  const hits = [];
26462
- for (const { element } of collectMarqueeHits(rect, iframe, overlayEl, activeCompositionPath)) {
26454
+ for (const { element } of hitsWithin(rect, candidates)) {
26463
26455
  const sel = await resolveDomEditSelection(element, {
26464
26456
  activeCompositionPath,
26465
26457
  isMasterView,
@@ -26473,13 +26465,15 @@ function useMarqueeGestures(deps) {
26473
26465
  const marqueeRef = useRef45(null);
26474
26466
  const [marqueeRect, setMarqueeRect] = useState29(null);
26475
26467
  const [candidateRects, setCandidateRects] = useState29([]);
26468
+ const candidatesRef = useRef45(null);
26476
26469
  const commitMarquee = useCallback43(
26477
26470
  async (rect, additive) => {
26478
26471
  const iframe = deps.iframeRef.current;
26479
26472
  const overlay = deps.overlayRef.current;
26480
26473
  if (!iframe || !overlay || !deps.onMarqueeSelectRef.current) return;
26481
26474
  const acp = deps.activeCompositionPathRef.current ?? "index.html";
26482
- const hits = await runMarqueeIntersection(rect, iframe, overlay, acp);
26475
+ const candidates = candidatesRef.current ?? collectMarqueeCandidates(iframe, overlay, acp);
26476
+ const hits = await runMarqueeIntersection(rect, candidates, acp);
26483
26477
  deps.onMarqueeSelectRef.current(hits, additive);
26484
26478
  },
26485
26479
  [deps.iframeRef, deps.overlayRef, deps.onMarqueeSelectRef, deps.activeCompositionPathRef]
@@ -26497,6 +26491,7 @@ function useMarqueeGestures(deps) {
26497
26491
  const dy = m.currentY - m.startY;
26498
26492
  if (Math.hypot(dx, dy) < MARQUEE_THRESHOLD_PX) return;
26499
26493
  m.pastThreshold = true;
26494
+ candidatesRef.current = null;
26500
26495
  }
26501
26496
  const rect = {
26502
26497
  left: Math.min(m.startX, m.currentX),
@@ -26509,7 +26504,8 @@ function useMarqueeGestures(deps) {
26509
26504
  const overlay = deps.overlayRef.current;
26510
26505
  if (iframe && overlay) {
26511
26506
  const acp = deps.activeCompositionPathRef.current ?? "index.html";
26512
- setCandidateRects(collectMarqueeHits(rect, iframe, overlay, acp).map((h) => h.rect));
26507
+ candidatesRef.current ??= collectMarqueeCandidates(iframe, overlay, acp);
26508
+ setCandidateRects(hitsWithin(rect, candidatesRef.current).map((h) => h.rect));
26513
26509
  }
26514
26510
  return;
26515
26511
  }
@@ -26541,6 +26537,7 @@ function useMarqueeGestures(deps) {
26541
26537
  }
26542
26538
  setMarqueeRect(null);
26543
26539
  setCandidateRects([]);
26540
+ candidatesRef.current = null;
26544
26541
  return;
26545
26542
  }
26546
26543
  deps.gestures.onPointerUp(event);
@@ -26552,6 +26549,7 @@ function useMarqueeGestures(deps) {
26552
26549
  marqueeRef.current = null;
26553
26550
  setMarqueeRect(null);
26554
26551
  setCandidateRects([]);
26552
+ candidatesRef.current = null;
26555
26553
  return;
26556
26554
  }
26557
26555
  deps.gestures.clearPointerState(deps.selectionRef);
@@ -26989,7 +26987,7 @@ function OffCanvasIndicators({
26989
26987
  const selectOffCanvas = async () => {
26990
26988
  const el = elements.current.get(r.key);
26991
26989
  if (!el) return;
26992
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-7AMZ7GFI.js");
26990
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-XRVXRTNX.js");
26993
26991
  const acp = activeCompositionPathRef.current ?? "index.html";
26994
26992
  const sel = await resolveDomEditSelection2(el, {
26995
26993
  activeCompositionPath: acp,
@@ -30590,9 +30588,14 @@ function observeDoc(doc, markDirty) {
30590
30588
  });
30591
30589
  return observer;
30592
30590
  }
30591
+ var RECOMPUTE_INTERVAL_MS = 100;
30592
+ function rebuildDue(dirty, lastAt, now2) {
30593
+ return dirty && now2 - lastAt >= RECOMPUTE_INTERVAL_MS;
30594
+ }
30593
30595
  function startOffCanvasIndicatorRefresh(options) {
30594
30596
  let frame = 0;
30595
30597
  let lastCompSig = "";
30598
+ let lastRecomputeAt = Number.NEGATIVE_INFINITY;
30596
30599
  const markDirty = () => {
30597
30600
  options.dirtyRef.current = true;
30598
30601
  };
@@ -30621,7 +30624,9 @@ function startOffCanvasIndicatorRefresh(options) {
30621
30624
  if (options.dirtyRef.current) clearIndicators(options);
30622
30625
  return;
30623
30626
  }
30624
- if (!options.dirtyRef.current) return;
30627
+ const now2 = performance.now();
30628
+ if (!rebuildDue(options.dirtyRef.current, lastRecomputeAt, now2)) return;
30629
+ lastRecomputeAt = now2;
30625
30630
  options.dirtyRef.current = false;
30626
30631
  recomputeOffCanvasIndicators(
30627
30632
  iframe,
@@ -35368,35 +35373,52 @@ function useElementLifecycleOps({
35368
35373
  commitDomEditPatchBatches,
35369
35374
  onElementDeleted
35370
35375
  }) {
35371
- const handleDomEditElementDelete = useCallback51(
35376
+ const handleDomEditElementsDelete = useCallback51(
35372
35377
  // fallow-ignore-next-line complexity
35373
- async (selection) => {
35378
+ async (selections) => {
35374
35379
  const pid = projectIdRef.current;
35375
35380
  if (!pid) return;
35376
- const label = selection.label || selection.id || selection.selector || selection.tagName;
35381
+ const [selection] = selections;
35382
+ if (!selection) return;
35383
+ const label = selections.length === 1 ? selection.label || selection.id || selection.selector || selection.tagName : `${selections.length} elements`;
35384
+ if (selections.length > 1) showToast(`Deleting ${label}...`, "info");
35377
35385
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
35386
+ const sameFile = selections.filter(
35387
+ (candidate) => (candidate.sourceFile || activeCompPath || "index.html") === targetPath
35388
+ );
35378
35389
  try {
35379
35390
  const originalContent = await readProjectFileContent(pid, targetPath);
35380
- const patchTarget = buildDomEditPatchTarget(selection);
35381
- if (!patchTarget.id && !patchTarget.selector && !patchTarget.hfId) {
35391
+ const patchTargets = sameFile.map((member) => buildDomEditPatchTarget(member));
35392
+ if (patchTargets.some((t) => !t.id && !t.selector && !t.hfId)) {
35382
35393
  throw new Error("Selected element has no patchable target");
35383
35394
  }
35384
- if (onTrySdkDelete && selection.hfId) {
35385
- const handled = await onTrySdkDelete(selection.hfId, originalContent, targetPath);
35386
- if (cutoverCommittedOrThrow(handled)) {
35395
+ const hfIds = sameFile.map((member) => member.hfId).filter((hfId) => Boolean(hfId));
35396
+ if (onTrySdkDelete && hfIds.length === sameFile.length) {
35397
+ let allHandled = true;
35398
+ for (const hfId of hfIds) {
35399
+ const handled = await onTrySdkDelete(hfId, originalContent, targetPath);
35400
+ if (!cutoverCommittedOrThrow(handled)) {
35401
+ allHandled = false;
35402
+ break;
35403
+ }
35404
+ }
35405
+ if (allHandled) {
35387
35406
  clearDomSelection();
35388
35407
  usePlayerStore.getState().setSelectedElementId(null);
35389
- showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
35408
+ showToast(
35409
+ `Deleted ${label}. Use Undo to restore ${sameFile.length === 1 ? "it" : "them"}.`,
35410
+ "info"
35411
+ );
35390
35412
  return;
35391
35413
  }
35392
35414
  }
35393
35415
  domEditSaveTimestampRef.current = Date.now();
35394
35416
  const removeResponse = await fetch(
35395
- `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
35417
+ `/api/projects/${pid}/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
35396
35418
  {
35397
35419
  method: "POST",
35398
35420
  headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
35399
- body: JSON.stringify({ target: patchTarget })
35421
+ body: JSON.stringify({ targets: patchTargets })
35400
35422
  }
35401
35423
  );
35402
35424
  if (!removeResponse.ok) {
@@ -35406,6 +35428,10 @@ function useElementLifecycleOps({
35406
35428
  );
35407
35429
  }
35408
35430
  const removeData = await removeResponse.json();
35431
+ if (!removeData.changed) {
35432
+ reloadPreview();
35433
+ throw new Error("Nothing to delete \u2014 the preview was out of date. Try again.");
35434
+ }
35409
35435
  const patchedContent = typeof removeData.content === "string" ? removeData.content : originalContent;
35410
35436
  await saveProjectFilesWithHistory({
35411
35437
  projectId: pid,
@@ -35423,8 +35449,11 @@ function useElementLifecycleOps({
35423
35449
  usePlayerStore.getState().setSelectedElementId(null);
35424
35450
  forceReloadSdkSession?.();
35425
35451
  reloadPreview();
35426
- onElementDeleted?.(selection);
35427
- showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
35452
+ for (const member of sameFile) onElementDeleted?.(member);
35453
+ showToast(
35454
+ `Deleted ${label}. Use Undo to restore ${sameFile.length === 1 ? "it" : "them"}.`,
35455
+ "info"
35456
+ );
35428
35457
  } catch (error) {
35429
35458
  const message = error instanceof Error ? error.message : "Failed to delete element";
35430
35459
  showToast(message);
@@ -35551,8 +35580,15 @@ function useElementLifecycleOps({
35551
35580
  },
35552
35581
  [commitDomEditPatchBatches, onReorderShadow]
35553
35582
  );
35583
+ const handleDomEditElementDelete = useCallback51(
35584
+ async (selection) => {
35585
+ await handleDomEditElementsDelete([selection]);
35586
+ },
35587
+ [handleDomEditElementsDelete]
35588
+ );
35554
35589
  return {
35555
35590
  handleDomEditElementDelete,
35591
+ handleDomEditElementsDelete,
35556
35592
  handleDomZIndexReorderCommit
35557
35593
  };
35558
35594
  }
@@ -37638,6 +37674,9 @@ function selectionIdsMatch(currentIds, selectedIds, currentAnchor, wantedAnchor)
37638
37674
  }
37639
37675
  return currentAnchor === wantedAnchor;
37640
37676
  }
37677
+ function anchorIsOutsideSelection(anchor2, selectedIds) {
37678
+ return anchor2 !== null && !selectedIds.includes(anchor2);
37679
+ }
37641
37680
  function useTimelineSelectionPreviewSync({
37642
37681
  selectedElementId,
37643
37682
  selectedElementIds,
@@ -37688,6 +37727,11 @@ function useTimelineSelectionPreviewSync({
37688
37727
  return;
37689
37728
  }
37690
37729
  let cancelled = false;
37730
+ const warnSelectionMissingOnce = () => {
37731
+ if (missingSelectionKeyRef.current === selectedKey) return;
37732
+ missingSelectionKeyRef.current = selectedKey;
37733
+ onSelectionNotFound();
37734
+ };
37691
37735
  const syncSelection = async () => {
37692
37736
  const selections = [];
37693
37737
  let resolvableCount = 0;
@@ -37700,9 +37744,9 @@ function useTimelineSelectionPreviewSync({
37700
37744
  }
37701
37745
  if (cancelled) return;
37702
37746
  if (selections.length < resolvableCount) {
37703
- if (missingSelectionKeyRef.current !== selectedKey) {
37704
- missingSelectionKeyRef.current = selectedKey;
37705
- onSelectionNotFound();
37747
+ warnSelectionMissingOnce();
37748
+ if (anchorIsOutsideSelection(currentAnchor, selectedIds)) {
37749
+ applyDomSelection(null, { revealPanel: false, announce: false });
37706
37750
  }
37707
37751
  return;
37708
37752
  }
@@ -39358,6 +39402,14 @@ import { resolveEditingSections } from "@hyperframes/core/editing";
39358
39402
 
39359
39403
  // src/components/editor/propertyPanelMediaSection.tsx
39360
39404
  import { useEffect as useEffect48, useState as useState47 } from "react";
39405
+ import {
39406
+ AUDIO_GAIN_FADER_MAX,
39407
+ AUDIO_GAIN_FADER_MIN,
39408
+ audioFaderPositionToGain,
39409
+ formatAudioGain,
39410
+ audioGainToFaderPosition,
39411
+ audioGainToText
39412
+ } from "@hyperframes/core/audio-gain";
39361
39413
  import { Fragment as Fragment25, jsx as jsx83, jsxs as jsxs61 } from "react/jsx-runtime";
39362
39414
  function MediaSection({
39363
39415
  projectDir,
@@ -39375,7 +39427,7 @@ function MediaSection({
39375
39427
  const isVisualMedia = isVideo || isImage;
39376
39428
  const el = element.element;
39377
39429
  const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
39378
- const volumePercent = Math.round(volume * 100);
39430
+ const volumeFaderPosition = audioGainToFaderPosition(volume);
39379
39431
  const mediaStart = Number.parseFloat(
39380
39432
  element.dataAttributes["media-start"] ?? element.dataAttributes["playback-start"] ?? "0"
39381
39433
  ) || 0;
@@ -39557,14 +39609,14 @@ function MediaSection({
39557
39609
  SliderControl,
39558
39610
  {
39559
39611
  trackName: "Volume",
39560
- value: volumePercent,
39561
- min: 0,
39562
- max: 100,
39612
+ value: volumeFaderPosition,
39613
+ min: AUDIO_GAIN_FADER_MIN,
39614
+ max: AUDIO_GAIN_FADER_MAX,
39563
39615
  step: 1,
39564
- displayValue: `${volumePercent}%`,
39565
- formatDisplayValue: (next) => `${Math.round(next)}%`,
39616
+ displayValue: audioGainToText(volume),
39617
+ formatDisplayValue: (next) => audioGainToText(audioFaderPositionToGain(next)),
39566
39618
  onCommit: (next) => {
39567
- void onSetAttribute("volume", formatNumericValue(next / 100));
39619
+ void onSetAttribute("volume", formatAudioGain(audioFaderPositionToGain(next)));
39568
39620
  }
39569
39621
  }
39570
39622
  )
@@ -51435,6 +51487,14 @@ function FlatToggle({
51435
51487
  }
51436
51488
 
51437
51489
  // src/components/editor/propertyPanelFlatMediaSection.tsx
51490
+ import {
51491
+ AUDIO_GAIN_FADER_MAX as AUDIO_GAIN_FADER_MAX2,
51492
+ AUDIO_GAIN_FADER_MIN as AUDIO_GAIN_FADER_MIN2,
51493
+ audioFaderPositionToGain as audioFaderPositionToGain2,
51494
+ formatAudioGain as formatAudioGain2,
51495
+ audioGainToFaderPosition as audioGainToFaderPosition2,
51496
+ audioGainToText as audioGainToText2
51497
+ } from "@hyperframes/core/audio-gain";
51438
51498
  import { Fragment as Fragment37, jsx as jsx131, jsxs as jsxs106 } from "react/jsx-runtime";
51439
51499
  function FlatMediaSection({
51440
51500
  projectDir,
@@ -51455,7 +51515,7 @@ function FlatMediaSection({
51455
51515
  const isVisualMedia = isVideo || isImage;
51456
51516
  const el = element.element;
51457
51517
  const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
51458
- const volumePercent = Math.round(volume * 100);
51518
+ const volumeFaderPosition = audioGainToFaderPosition2(volume);
51459
51519
  const mediaStart = Number.parseFloat(
51460
51520
  element.dataAttributes["media-start"] ?? element.dataAttributes["playback-start"] ?? "0"
51461
51521
  ) || 0;
@@ -51598,13 +51658,14 @@ function FlatMediaSection({
51598
51658
  FlatSlider,
51599
51659
  {
51600
51660
  label: "Volume",
51601
- value: volumePercent,
51602
- min: 0,
51603
- max: 100,
51604
- tier: volumePercent === 100 ? "default" : "explicitCustom",
51605
- displayValue: `${volumePercent}%`,
51661
+ value: volumeFaderPosition,
51662
+ min: AUDIO_GAIN_FADER_MIN2,
51663
+ max: AUDIO_GAIN_FADER_MAX2,
51664
+ tier: volume === 1 ? "default" : "explicitCustom",
51665
+ displayValue: audioGainToText2(volume),
51606
51666
  disabled: volumeAutomated,
51607
- onCommit: (next) => void onSetAttribute("volume", formatNumericValue(next / 100))
51667
+ centerTick: true,
51668
+ onCommit: (next) => void onSetAttribute("volume", formatAudioGain2(audioFaderPositionToGain2(next)))
51608
51669
  }
51609
51670
  ) }),
51610
51671
  /* @__PURE__ */ jsx131(
@@ -61820,36 +61881,44 @@ function useTimelineEditing({
61820
61881
  isRecordingRef,
61821
61882
  forceReloadSdkSession
61822
61883
  });
61823
- const handleTimelineElementDelete = useCallback91(
61884
+ const handleTimelineElementsDelete = useCallback91(
61824
61885
  // fallow-ignore-next-line complexity
61825
- async (element) => {
61886
+ async (selection) => {
61826
61887
  if (isRecordingRef?.current) {
61827
61888
  showToast("Cannot edit timeline while recording", "error");
61828
61889
  return;
61829
61890
  }
61830
61891
  const pid = projectIdRef.current;
61831
61892
  if (!pid) throw new Error("No active project");
61832
- const label = getTimelineElementLabel(element);
61893
+ const [element] = selection;
61894
+ if (!element) return;
61895
+ const label = selection.length === 1 ? getTimelineElementLabel(element) : `${selection.length} clips`;
61833
61896
  const targetPath = element.sourceFile || activeCompPath || "index.html";
61897
+ const sameFile = selection.filter(
61898
+ (candidate) => (candidate.sourceFile || activeCompPath || "index.html") === targetPath
61899
+ );
61834
61900
  try {
61835
61901
  const originalContent = await readFileContent(pid, targetPath);
61836
- const patchTarget = buildPatchTarget(element);
61837
- if (!patchTarget) {
61838
- throw new Error(`Timeline element ${element.id} is missing a patchable target`);
61839
- }
61840
- const removeResponse = await fetch(
61841
- `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
61842
- {
61843
- method: "POST",
61844
- headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
61845
- body: JSON.stringify({ target: patchTarget })
61902
+ let removedContent = originalContent;
61903
+ for (const target of sameFile) {
61904
+ const patchTarget = buildPatchTarget(target);
61905
+ if (!patchTarget) {
61906
+ throw new Error(`Timeline element ${target.id} is missing a patchable target`);
61846
61907
  }
61847
- );
61848
- if (!removeResponse.ok) {
61849
- throw new Error(`Failed to delete ${element.id} from ${targetPath}`);
61908
+ const removeResponse = await fetch(
61909
+ `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
61910
+ {
61911
+ method: "POST",
61912
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
61913
+ body: JSON.stringify({ target: patchTarget })
61914
+ }
61915
+ );
61916
+ if (!removeResponse.ok) {
61917
+ throw new Error(`Failed to delete ${target.id} from ${targetPath}`);
61918
+ }
61919
+ const removeData = await removeResponse.json();
61920
+ if (typeof removeData.content === "string") removedContent = removeData.content;
61850
61921
  }
61851
- const removeData = await removeResponse.json();
61852
- const removedContent = typeof removeData.content === "string" ? removeData.content : originalContent;
61853
61922
  const deleteContentEnd = furthestClipEndFromSource(removedContent);
61854
61923
  const patchedContent = setCompositionDurationToContent(removedContent, deleteContentEnd);
61855
61924
  const rollbackDuration = captureDurationRollback(previewIframeRef.current);
@@ -61874,13 +61943,16 @@ function useTimelineEditing({
61874
61943
  rollbackDuration();
61875
61944
  throw error;
61876
61945
  }
61877
- usePlayerStore.getState().setElements(
61878
- timelineElements.filter((te) => (te.key ?? te.id) !== (element.key ?? element.id))
61879
- );
61946
+ const deletedKeys = new Set(sameFile.map((te) => te.key ?? te.id));
61947
+ usePlayerStore.getState().setElements(timelineElements.filter((te) => !deletedKeys.has(te.key ?? te.id)));
61880
61948
  usePlayerStore.getState().setSelectedElementId(null);
61949
+ usePlayerStore.getState().setSelectedElementIds(/* @__PURE__ */ new Set());
61881
61950
  forceReloadSdkSession?.();
61882
61951
  reloadPreview();
61883
- showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
61952
+ showToast(
61953
+ `Deleted ${label}. Use Undo to restore ${sameFile.length === 1 ? "it" : "them"}.`,
61954
+ "info"
61955
+ );
61884
61956
  } catch (error) {
61885
61957
  const message = error instanceof Error ? error.message : "Failed to delete timeline clip";
61886
61958
  showToast(message);
@@ -61899,6 +61971,12 @@ function useTimelineEditing({
61899
61971
  previewIframeRef
61900
61972
  ]
61901
61973
  );
61974
+ const handleTimelineElementDelete = useCallback91(
61975
+ async (element) => {
61976
+ await handleTimelineElementsDelete([element]);
61977
+ },
61978
+ [handleTimelineElementsDelete]
61979
+ );
61902
61980
  const { handleTimelineAssetDrop, handleTimelineFileDrop, handleTimelineCompositionDrop } = useTimelineAssetDropOps({
61903
61981
  projectIdRef,
61904
61982
  activeCompPath,
@@ -61940,6 +62018,7 @@ function useTimelineEditing({
61940
62018
  handleToggleTrackHidden,
61941
62019
  handleToggleElementHidden,
61942
62020
  handleTimelineElementDelete,
62021
+ handleTimelineElementsDelete,
61943
62022
  handleTimelineElementSplit: handleRazorSplit,
61944
62023
  handleRazorSplit,
61945
62024
  handleRazorSplitAll,
@@ -62341,7 +62420,7 @@ function useDomSelection({
62341
62420
  domEditGroupSelectionsRef.current = [];
62342
62421
  setDomEditSelection(null);
62343
62422
  setDomEditGroupSelections([]);
62344
- announceTimelineSelection2([], null);
62423
+ if (options?.announce !== false) announceTimelineSelection2([], null);
62345
62424
  return;
62346
62425
  }
62347
62426
  const isAdditiveSelection = Boolean(options?.additive);
@@ -62487,7 +62566,11 @@ function useDomSelection({
62487
62566
  }
62488
62567
  const selection = await buildDomSelectionForTimelineElement(element);
62489
62568
  if (seq !== timelineSelectSeqRef.current) return;
62490
- if (selection) applyDomSelection(selection);
62569
+ if (selection) {
62570
+ applyDomSelection(selection);
62571
+ return;
62572
+ }
62573
+ applyDomSelection(null, { revealPanel: false, announce: false });
62491
62574
  },
62492
62575
  [applyDomSelection, buildDomSelectionForTimelineElement]
62493
62576
  );
@@ -64463,7 +64546,7 @@ function useDomEditCommits({
64463
64546
  showToast,
64464
64547
  commitPositionPatchToHtml
64465
64548
  });
64466
- const { handleDomEditElementDelete, handleDomZIndexReorderCommit } = useElementLifecycleOps({
64549
+ const { handleDomEditElementDelete, handleDomEditElementsDelete, handleDomZIndexReorderCommit } = useElementLifecycleOps({
64467
64550
  activeCompPath,
64468
64551
  showToast,
64469
64552
  writeProjectFile,
@@ -64496,6 +64579,7 @@ function useDomEditCommits({
64496
64579
  handleDomRotationCommit,
64497
64580
  handleDomManualEditsReset,
64498
64581
  handleDomEditElementDelete,
64582
+ handleDomEditElementsDelete,
64499
64583
  handleDomZIndexReorderCommit
64500
64584
  };
64501
64585
  }
@@ -66051,7 +66135,7 @@ function useDomEditPreviewSync({
66051
66135
  refreshDomEditGroupSelectionsFromPreview,
66052
66136
  buildDomSelectionFromTarget,
66053
66137
  refreshPreviewDocumentVersion,
66054
- syncPreviewHistoryHotkey,
66138
+ syncPreviewHotkeys,
66055
66139
  applyStudioManualEditsToPreviewRef,
66056
66140
  openSourceForSelection,
66057
66141
  getSidebarTab,
@@ -66090,12 +66174,12 @@ function useDomEditPreviewSync({
66090
66174
  applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
66091
66175
  }
66092
66176
  };
66093
- syncPreviewHistoryHotkey(previewIframe);
66177
+ syncPreviewHotkeys(previewIframe);
66094
66178
  void applyStudioManualEditsToPreviewRef.current(previewIframe);
66095
66179
  void syncSelectionFromDocument();
66096
66180
  refreshPreviewDocumentVersion();
66097
66181
  const handleLoad = () => {
66098
- syncPreviewHistoryHotkey(previewIframe);
66182
+ syncPreviewHotkeys(previewIframe);
66099
66183
  void applyStudioManualEditsToPreviewRef.current(previewIframe);
66100
66184
  void syncSelectionFromDocument();
66101
66185
  refreshPreviewDocumentVersion();
@@ -66114,7 +66198,7 @@ function useDomEditPreviewSync({
66114
66198
  previewIframe,
66115
66199
  refreshDomEditGroupSelectionsFromPreview,
66116
66200
  refreshPreviewDocumentVersion,
66117
- syncPreviewHistoryHotkey,
66201
+ syncPreviewHotkeys,
66118
66202
  applyStudioManualEditsToPreviewRef,
66119
66203
  gsapCacheVersion
66120
66204
  ]);
@@ -66522,7 +66606,7 @@ function useDomEditWiring({
66522
66606
  bumpGsapCache,
66523
66607
  showToast,
66524
66608
  refreshPreviewDocumentVersion,
66525
- syncPreviewHistoryHotkey,
66609
+ syncPreviewHotkeys,
66526
66610
  applyStudioManualEditsToPreviewRef,
66527
66611
  applyDomSelection,
66528
66612
  buildDomSelectionFromTarget,
@@ -66633,7 +66717,7 @@ function useDomEditWiring({
66633
66717
  refreshDomEditGroupSelectionsFromPreview,
66634
66718
  buildDomSelectionFromTarget,
66635
66719
  refreshPreviewDocumentVersion,
66636
- syncPreviewHistoryHotkey,
66720
+ syncPreviewHotkeys,
66637
66721
  applyStudioManualEditsToPreviewRef,
66638
66722
  openSourceForSelection,
66639
66723
  getSidebarTab,
@@ -67849,6 +67933,9 @@ function useKeyframeEaseCommits({
67849
67933
  }
67850
67934
 
67851
67935
  // src/hooks/useDomEditSession.ts
67936
+ function membersForDelete(selection, group, options) {
67937
+ return options?.expandGroup && group.length > 0 ? group : [selection];
67938
+ }
67852
67939
  function useDomEditSession({
67853
67940
  projectId,
67854
67941
  activeCompPath,
@@ -67863,6 +67950,7 @@ function useDomEditSession({
67863
67950
  setRightCollapsed,
67864
67951
  setRightPanelTab,
67865
67952
  showToast,
67953
+ isRecordingRef,
67866
67954
  refreshPreviewDocumentVersion,
67867
67955
  queueDomEditSave,
67868
67956
  readProjectFile,
@@ -67879,7 +67967,7 @@ function useDomEditSession({
67879
67967
  previewDocumentVersion,
67880
67968
  rightPanelTab,
67881
67969
  applyStudioManualEditsToPreviewRef,
67882
- syncPreviewHistoryHotkey,
67970
+ syncPreviewHotkeys,
67883
67971
  reloadPreview,
67884
67972
  setRefreshKey: _setRefreshKey,
67885
67973
  openSourceForSelection,
@@ -68006,7 +68094,7 @@ function useDomEditSession({
68006
68094
  handleDomRemoveTextField,
68007
68095
  handleDomBoxSizeCommit,
68008
68096
  handleDomManualEditsReset,
68009
- handleDomEditElementDelete,
68097
+ handleDomEditElementsDelete,
68010
68098
  handleDomZIndexReorderCommit
68011
68099
  } = useDomEditCommits({
68012
68100
  activeCompPath,
@@ -68080,6 +68168,17 @@ function useDomEditSession({
68080
68168
  clearDomSelection,
68081
68169
  forceReloadSdkSession
68082
68170
  });
68171
+ const handleDomEditElementDelete = useCallback114(
68172
+ async (selection, options) => {
68173
+ if (isRecordingRef?.current) {
68174
+ showToast("Cannot edit timeline while recording", "error");
68175
+ return;
68176
+ }
68177
+ const members = membersForDelete(selection, domEditGroupSelectionsRef.current, options);
68178
+ await handleDomEditElementsDelete(members);
68179
+ },
68180
+ [domEditGroupSelectionsRef, handleDomEditElementsDelete, isRecordingRef, showToast]
68181
+ );
68083
68182
  const handleGroupSelection = useCallback114(() => {
68084
68183
  const group = domEditGroupSelectionsRef.current;
68085
68184
  const single = domEditSelectionRef.current;
@@ -68143,7 +68242,7 @@ function useDomEditSession({
68143
68242
  bumpGsapCache,
68144
68243
  showToast,
68145
68244
  refreshPreviewDocumentVersion,
68146
- syncPreviewHistoryHotkey,
68245
+ syncPreviewHotkeys,
68147
68246
  applyStudioManualEditsToPreviewRef,
68148
68247
  applyDomSelection,
68149
68248
  buildDomSelectionFromTarget,
@@ -69437,21 +69536,19 @@ function dispatchPlainKey(event, key2, cb) {
69437
69536
  return;
69438
69537
  }
69439
69538
  }
69539
+ const domSel = cb.domEditSelectionRef.current;
69540
+ if (domSel) {
69541
+ event.preventDefault();
69542
+ void cb.handleDomEditElementDelete(domSel, { expandGroup: true });
69543
+ return;
69544
+ }
69440
69545
  const { selectedElementId, selectedElementIds, elements } = usePlayerStore.getState();
69441
69546
  const selectionKeys = new Set(selectedElementIds);
69442
69547
  if (selectedElementId) selectionKeys.add(selectedElementId);
69443
- if (selectionKeys.size > 0) {
69444
- const el = elements.find((e) => selectionKeys.has(e.key ?? e.id));
69445
- if (el) {
69446
- event.preventDefault();
69447
- void cb.handleTimelineElementDelete(el);
69448
- return;
69449
- }
69450
- }
69451
- const domSel = cb.domEditSelectionRef.current;
69452
- if (domSel) {
69548
+ const selected = elements.filter((e) => selectionKeys.has(e.key ?? e.id));
69549
+ if (selected.length > 0) {
69453
69550
  event.preventDefault();
69454
- void cb.handleDomEditElementDelete(domSel);
69551
+ void cb.handleTimelineElementsDelete(selected);
69455
69552
  }
69456
69553
  return;
69457
69554
  }
@@ -69461,7 +69558,7 @@ function dispatchPlainKey(event, key2, cb) {
69461
69558
  }
69462
69559
  }
69463
69560
  function useAppHotkeys({
69464
- handleTimelineElementDelete,
69561
+ handleTimelineElementsDelete,
69465
69562
  handleTimelineElementSplit,
69466
69563
  handleDomEditElementDelete,
69467
69564
  domEditSelectionRef,
@@ -69486,7 +69583,6 @@ function useAppHotkeys({
69486
69583
  activeCompPath,
69487
69584
  forceReloadSdkSession
69488
69585
  }) {
69489
- const previewHotkeyWindowRef = useRef126(null);
69490
69586
  const previewHistoryCleanupRef = useRef126(null);
69491
69587
  const readHistoryFile = useCallback119(
69492
69588
  (path) => path === STUDIO_MOTION_PATH ? readOptionalProjectFile(path) : readProjectFile(path),
@@ -69545,7 +69641,7 @@ function useAppHotkeys({
69545
69641
  const handleRedo = useCallback119(() => applyHistory("redo"), [applyHistory]);
69546
69642
  const cbRef = useRef126(null);
69547
69643
  cbRef.current = {
69548
- handleTimelineElementDelete,
69644
+ handleTimelineElementsDelete,
69549
69645
  handleTimelineElementSplit,
69550
69646
  handleDomEditElementDelete,
69551
69647
  handleUndo,
@@ -69575,31 +69671,6 @@ function useAppHotkeys({
69575
69671
  window.addEventListener("keydown", handleAppKeyDown, true);
69576
69672
  return () => window.removeEventListener("keydown", handleAppKeyDown, true);
69577
69673
  }, [handleAppKeyDown]);
69578
- const syncPreviewTimelineHotkey = useCallback119(
69579
- (iframe) => {
69580
- const nextWindow = iframeContentWindow(iframe);
69581
- if (previewHotkeyWindowRef.current === nextWindow) return;
69582
- safeRemoveListener(
69583
- previewHotkeyWindowRef.current,
69584
- "keydown",
69585
- handleAppKeyDown
69586
- );
69587
- previewHotkeyWindowRef.current = nextWindow;
69588
- safeAddListener(nextWindow, "keydown", handleAppKeyDown, true);
69589
- },
69590
- [handleAppKeyDown]
69591
- );
69592
- useEffect99(
69593
- () => () => {
69594
- safeRemoveListener(
69595
- previewHotkeyWindowRef.current,
69596
- "keydown",
69597
- handleAppKeyDown
69598
- );
69599
- previewHotkeyWindowRef.current = null;
69600
- },
69601
- [handleAppKeyDown]
69602
- );
69603
69674
  const handleHistoryHotkey = useCallback119((event) => {
69604
69675
  if (!(event.metaKey || event.ctrlKey) || shouldIgnoreHistoryShortcut(event.target)) return;
69605
69676
  handleUndoRedoKey(
@@ -69608,7 +69679,7 @@ function useAppHotkeys({
69608
69679
  () => void cbRef.current.handleRedo()
69609
69680
  );
69610
69681
  }, []);
69611
- const syncPreviewHistoryHotkey = useCallback119(
69682
+ const syncPreviewHotkeys = useCallback119(
69612
69683
  (iframe) => {
69613
69684
  previewHistoryCleanupRef.current?.();
69614
69685
  previewHistoryCleanupRef.current = null;
@@ -69621,14 +69692,17 @@ function useAppHotkeys({
69621
69692
  }
69622
69693
  if (!win && !doc) return;
69623
69694
  const handler = handleHistoryHotkey;
69695
+ const appHandler = handleAppKeyDown;
69624
69696
  safeAddListener(win, "keydown", handler, true);
69697
+ safeAddListener(win, "keydown", appHandler, true);
69625
69698
  doc?.addEventListener("keydown", handleHistoryHotkey, true);
69626
69699
  previewHistoryCleanupRef.current = () => {
69627
69700
  safeRemoveListener(win, "keydown", handler);
69701
+ safeRemoveListener(win, "keydown", appHandler);
69628
69702
  doc?.removeEventListener("keydown", handleHistoryHotkey, true);
69629
69703
  };
69630
69704
  },
69631
- [handleHistoryHotkey]
69705
+ [handleAppKeyDown, handleHistoryHotkey]
69632
69706
  );
69633
69707
  useEffect99(
69634
69708
  () => () => {
@@ -69640,8 +69714,7 @@ function useAppHotkeys({
69640
69714
  return {
69641
69715
  handleUndo,
69642
69716
  handleRedo,
69643
- syncPreviewTimelineHotkey,
69644
- syncPreviewHistoryHotkey
69717
+ syncPreviewHotkeys
69645
69718
  };
69646
69719
  }
69647
69720
 
@@ -71207,7 +71280,7 @@ function findUrlSelectionElement(doc, target, fallbackSourceFile, activeCompPath
71207
71280
  }
71208
71281
  async function buildOptionalDomSelection(element, buildDomSelection) {
71209
71282
  if (!element) return null;
71210
- return buildDomSelection(element, { preferClipAncestor: false });
71283
+ return buildDomSelection(element, { preferClipAncestor: false, skipSourceProbe: true });
71211
71284
  }
71212
71285
  async function resolveUrlSelections({
71213
71286
  doc,
@@ -75568,6 +75641,7 @@ function getVisibleLayers(layers, collapsed) {
75568
75641
 
75569
75642
  // src/components/editor/LayersPanel.tsx
75570
75643
  import { jsx as jsx175, jsxs as jsxs145 } from "react/jsx-runtime";
75644
+ var LAYERS_PANEL_MAX_ROWS = 80;
75571
75645
  var TAG_ICONS = {
75572
75646
  video: "Vi",
75573
75647
  audio: "Au",
@@ -75652,11 +75726,13 @@ var LayersPanel = memo43(function LayersPanel2() {
75652
75726
  const root = doc.querySelector("[data-composition-id]") ?? doc.documentElement ?? null;
75653
75727
  if (!root) return;
75654
75728
  if (activeGroupElement && !activeGroupElement.isConnected) setActiveGroupElement(null);
75655
- const items = collectDomEditLayerItems(root, {
75656
- activeCompositionPath: activeCompPath,
75657
- isMasterView,
75658
- activeGroupElement
75659
- });
75729
+ const items = collectDomEditLayerItems(
75730
+ root,
75731
+ { activeCompositionPath: activeCompPath, isMasterView, activeGroupElement },
75732
+ // How many rows this panel is willing to render, nothing more. Hit-testing
75733
+ // callers deliberately take the whole document instead.
75734
+ LAYERS_PANEL_MAX_ROWS
75735
+ );
75660
75736
  setLayers(sortLayersByZIndex(items));
75661
75737
  }, [previewIframeRef, activeCompPath, isMasterView, activeGroupElement, setActiveGroupElement]);
75662
75738
  useEffect114(() => {
@@ -80959,11 +81035,11 @@ function FramePoster({
80959
81035
  src,
80960
81036
  seconds,
80961
81037
  title,
80962
- fit = "cover",
81038
+ surface = "tile",
80963
81039
  posterVersion
80964
81040
  }) {
80965
81041
  const [failed2, setFailed] = useState140(false);
80966
- useEffect126(() => setFailed(false), [src, seconds, posterVersion]);
81042
+ useEffect126(() => setFailed(false), [src, seconds, posterVersion, surface]);
80967
81043
  if (failed2) {
80968
81044
  return /* @__PURE__ */ jsx195("div", { className: "flex h-full w-full items-center justify-center text-[11px] text-neutral-600", children: "Preview unavailable" });
80969
81045
  }
@@ -80971,7 +81047,11 @@ function FramePoster({
80971
81047
  previewUrl: `/api/projects/${projectId}/preview/comp/${src}`,
80972
81048
  seekTime: seconds,
80973
81049
  duration: 0,
80974
- origin: window.location.origin
81050
+ origin: window.location.origin,
81051
+ // The normal 240x135 preview is unreadable in the contact sheet, while source
81052
+ // density is unbounded across all tiles. Give tiles a capped review density
81053
+ // and reserve true source output for the single focus hero.
81054
+ output: surface === "hero" ? "source" : "storyboard"
80975
81055
  });
80976
81056
  if (posterVersion) {
80977
81057
  const withVersion = new URL(url, window.location.origin);
@@ -80986,7 +81066,7 @@ function FramePoster({
80986
81066
  draggable: false,
80987
81067
  loading: "lazy",
80988
81068
  onError: () => setFailed(true),
80989
- className: `h-full w-full ${fit === "contain" ? "object-contain" : "object-cover"}`
81069
+ className: `h-full w-full ${surface === "hero" ? "object-contain" : "object-cover"}`
80990
81070
  }
80991
81071
  );
80992
81072
  }
@@ -81507,7 +81587,7 @@ function StoryboardFrameFocus({
81507
81587
  src: frame.src,
81508
81588
  seconds: posterTime(frame),
81509
81589
  title,
81510
- fit: "contain",
81590
+ surface: "hero",
81511
81591
  posterVersion
81512
81592
  }
81513
81593
  ) : /* @__PURE__ */ jsx201(FramePlan, { frame }) }) }),
@@ -82607,11 +82687,9 @@ function StudioApp() {
82607
82687
  const clearDomSelectionRef = useRef157(() => {
82608
82688
  });
82609
82689
  const domEditSelectionBridgeRef = useRef157(null);
82610
- const handleDomEditElementDeleteRef = useRef157(
82611
- async () => {
82612
- }
82613
- );
82614
- const domEditDeleteBridge = (s) => handleDomEditElementDeleteRef.current(s);
82690
+ const handleDomEditElementDeleteRef = useRef157(async () => {
82691
+ });
82692
+ const domEditDeleteBridge = (s, o) => handleDomEditElementDeleteRef.current(s, o);
82615
82693
  const resetKeyframesRef = useRef157(() => false);
82616
82694
  const deleteSelectedKeyframesRef = useRef157(() => {
82617
82695
  });
@@ -82629,7 +82707,7 @@ function StudioApp() {
82629
82707
  previewIframeRef
82630
82708
  });
82631
82709
  const appHotkeys = useAppHotkeys({
82632
- handleTimelineElementDelete: timelineEditing.handleTimelineElementDelete,
82710
+ handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
82633
82711
  handleTimelineElementSplit: timelineEditing.handleTimelineElementSplit,
82634
82712
  handleDomEditElementDelete: domEditDeleteBridge,
82635
82713
  domEditSelectionRef: domEditSelectionBridgeRef,
@@ -82673,6 +82751,7 @@ function StudioApp() {
82673
82751
  setRightCollapsed: panelLayout.setRightCollapsed,
82674
82752
  setRightPanelTab: panelLayout.setRightPanelTab,
82675
82753
  showToast,
82754
+ isRecordingRef: isGestureRecordingRef,
82676
82755
  refreshPreviewDocumentVersion,
82677
82756
  queueDomEditSave: previewPersistence.queueDomEditSave,
82678
82757
  readProjectFile: fileManager.readProjectFile,
@@ -82689,7 +82768,7 @@ function StudioApp() {
82689
82768
  previewDocumentVersion,
82690
82769
  rightPanelTab: panelLayout.rightPanelTab,
82691
82770
  applyStudioManualEditsToPreviewRef: previewPersistence.applyStudioManualEditsToPreviewRef,
82692
- syncPreviewHistoryHotkey: appHotkeys.syncPreviewHistoryHotkey,
82771
+ syncPreviewHotkeys: appHotkeys.syncPreviewHotkeys,
82693
82772
  reloadPreview,
82694
82773
  setRefreshKey,
82695
82774
  openSourceForSelection: fileManager.openSourceForSelection,
@@ -82755,7 +82834,6 @@ function StudioApp() {
82755
82834
  isGestureRecordingRef
82756
82835
  });
82757
82836
  handleToggleRecordingRef.current = handleToggleRecording;
82758
- const recordingToggle = handleToggleRecording;
82759
82837
  const canvasRectRef = useRef157(null);
82760
82838
  useLayoutEffect9(() => {
82761
82839
  if (gestureState !== "recording" || !previewIframe) {
@@ -82768,8 +82846,7 @@ function StudioApp() {
82768
82846
  (iframe) => {
82769
82847
  previewIframeRef.current = iframe;
82770
82848
  setPreviewIframe(iframe);
82771
- appHotkeys.syncPreviewTimelineHotkey(iframe);
82772
- appHotkeys.syncPreviewHistoryHotkey(iframe);
82849
+ appHotkeys.syncPreviewHotkeys(iframe);
82773
82850
  resetConsoleErrors();
82774
82851
  refreshPreviewDocumentVersion();
82775
82852
  },
@@ -82920,7 +82997,7 @@ function StudioApp() {
82920
82997
  },
82921
82998
  recordingState: gestureState,
82922
82999
  recordingDuration: gestureRecording.recordingDuration,
82923
- onToggleRecording: recordingToggle,
83000
+ onToggleRecording: handleToggleRecording,
82924
83001
  sdkSession: sdkHandle.session,
82925
83002
  publishSdkSession: sdkHandle.publish,
82926
83003
  forceReloadSdkSession: sdkHandle.forceReload,
@@ -82954,7 +83031,7 @@ function StudioApp() {
82954
83031
  shouldShowSelectedDomBounds,
82955
83032
  isGestureRecording: gestureState === "recording",
82956
83033
  recordingState: gestureState,
82957
- onToggleRecording: recordingToggle,
83034
+ onToggleRecording: handleToggleRecording,
82958
83035
  blockPreview,
82959
83036
  gestureOverlay: gestureState === "recording" && previewIframe ? /* @__PURE__ */ jsx206(
82960
83037
  GestureTrailOverlay,