hyperframes 0.8.2 → 0.8.4

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";
@@ -23822,20 +23822,6 @@ var NLEPreview = memo19(function NLEPreview2({
23822
23822
  document.addEventListener("wheel", handleWheel, { passive: false, capture: true });
23823
23823
  return () => document.removeEventListener("wheel", handleWheel, { capture: true });
23824
23824
  }, [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
23825
  useEffect27(() => {
23840
23826
  const isInsideViewport = (clientX, clientY) => {
23841
23827
  const viewport = viewportRef.current;
@@ -26425,7 +26411,7 @@ function resolveElementForOverlay(doc, sel, activeCompositionPath, cacheRef) {
26425
26411
 
26426
26412
  // src/components/editor/marqueeCommit.ts
26427
26413
  var MARQUEE_THRESHOLD_PX = 4;
26428
- function collectMarqueeHits(rect, iframe, overlayEl, activeCompositionPath) {
26414
+ function collectMarqueeCandidates(iframe, overlayEl, activeCompositionPath) {
26429
26415
  const doc = iframe.contentDocument;
26430
26416
  if (!doc) return [];
26431
26417
  const root = doc.querySelector("[data-composition-id]") ?? doc.body;
@@ -26438,28 +26424,32 @@ function collectMarqueeHits(rect, iframe, overlayEl, activeCompositionPath) {
26438
26424
  width: declW > 0 ? declW : rootEl.getBoundingClientRect().width || 1,
26439
26425
  height: declH > 0 ? declH : rootEl.getBoundingClientRect().height || 1
26440
26426
  };
26441
- const hits = [];
26427
+ const candidates = [];
26442
26428
  for (const item of items) {
26443
26429
  const el = item.element;
26444
26430
  if (!isElementComputedVisible(el)) continue;
26445
26431
  if (coversComposition(el.getBoundingClientRect(), viewport)) continue;
26446
26432
  const overlayRect = toVisibleOverlayRect(overlayEl, iframe, el);
26447
26433
  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 });
26434
+ candidates.push({
26435
+ element: el,
26436
+ rect: {
26437
+ left: overlayRect.left,
26438
+ top: overlayRect.top,
26439
+ width: overlayRect.width,
26440
+ height: overlayRect.height
26441
+ }
26442
+ });
26456
26443
  }
26457
- return hits;
26444
+ return candidates;
26445
+ }
26446
+ function hitsWithin(rect, candidates) {
26447
+ return candidates.filter((candidate) => rectsOverlap(rect, candidate.rect));
26458
26448
  }
26459
- async function runMarqueeIntersection(rect, iframe, overlayEl, activeCompositionPath) {
26449
+ async function runMarqueeIntersection(rect, candidates, activeCompositionPath) {
26460
26450
  const isMasterView = !activeCompositionPath || activeCompositionPath === "index.html";
26461
26451
  const hits = [];
26462
- for (const { element } of collectMarqueeHits(rect, iframe, overlayEl, activeCompositionPath)) {
26452
+ for (const { element } of hitsWithin(rect, candidates)) {
26463
26453
  const sel = await resolveDomEditSelection(element, {
26464
26454
  activeCompositionPath,
26465
26455
  isMasterView,
@@ -26473,13 +26463,15 @@ function useMarqueeGestures(deps) {
26473
26463
  const marqueeRef = useRef45(null);
26474
26464
  const [marqueeRect, setMarqueeRect] = useState29(null);
26475
26465
  const [candidateRects, setCandidateRects] = useState29([]);
26466
+ const candidatesRef = useRef45(null);
26476
26467
  const commitMarquee = useCallback43(
26477
26468
  async (rect, additive) => {
26478
26469
  const iframe = deps.iframeRef.current;
26479
26470
  const overlay = deps.overlayRef.current;
26480
26471
  if (!iframe || !overlay || !deps.onMarqueeSelectRef.current) return;
26481
26472
  const acp = deps.activeCompositionPathRef.current ?? "index.html";
26482
- const hits = await runMarqueeIntersection(rect, iframe, overlay, acp);
26473
+ const candidates = candidatesRef.current ?? collectMarqueeCandidates(iframe, overlay, acp);
26474
+ const hits = await runMarqueeIntersection(rect, candidates, acp);
26483
26475
  deps.onMarqueeSelectRef.current(hits, additive);
26484
26476
  },
26485
26477
  [deps.iframeRef, deps.overlayRef, deps.onMarqueeSelectRef, deps.activeCompositionPathRef]
@@ -26497,6 +26489,7 @@ function useMarqueeGestures(deps) {
26497
26489
  const dy = m.currentY - m.startY;
26498
26490
  if (Math.hypot(dx, dy) < MARQUEE_THRESHOLD_PX) return;
26499
26491
  m.pastThreshold = true;
26492
+ candidatesRef.current = null;
26500
26493
  }
26501
26494
  const rect = {
26502
26495
  left: Math.min(m.startX, m.currentX),
@@ -26509,7 +26502,8 @@ function useMarqueeGestures(deps) {
26509
26502
  const overlay = deps.overlayRef.current;
26510
26503
  if (iframe && overlay) {
26511
26504
  const acp = deps.activeCompositionPathRef.current ?? "index.html";
26512
- setCandidateRects(collectMarqueeHits(rect, iframe, overlay, acp).map((h) => h.rect));
26505
+ candidatesRef.current ??= collectMarqueeCandidates(iframe, overlay, acp);
26506
+ setCandidateRects(hitsWithin(rect, candidatesRef.current).map((h) => h.rect));
26513
26507
  }
26514
26508
  return;
26515
26509
  }
@@ -26541,6 +26535,7 @@ function useMarqueeGestures(deps) {
26541
26535
  }
26542
26536
  setMarqueeRect(null);
26543
26537
  setCandidateRects([]);
26538
+ candidatesRef.current = null;
26544
26539
  return;
26545
26540
  }
26546
26541
  deps.gestures.onPointerUp(event);
@@ -26552,6 +26547,7 @@ function useMarqueeGestures(deps) {
26552
26547
  marqueeRef.current = null;
26553
26548
  setMarqueeRect(null);
26554
26549
  setCandidateRects([]);
26550
+ candidatesRef.current = null;
26555
26551
  return;
26556
26552
  }
26557
26553
  deps.gestures.clearPointerState(deps.selectionRef);
@@ -26989,7 +26985,7 @@ function OffCanvasIndicators({
26989
26985
  const selectOffCanvas = async () => {
26990
26986
  const el = elements.current.get(r.key);
26991
26987
  if (!el) return;
26992
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-7AMZ7GFI.js");
26988
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-XRVXRTNX.js");
26993
26989
  const acp = activeCompositionPathRef.current ?? "index.html";
26994
26990
  const sel = await resolveDomEditSelection2(el, {
26995
26991
  activeCompositionPath: acp,
@@ -30590,9 +30586,14 @@ function observeDoc(doc, markDirty) {
30590
30586
  });
30591
30587
  return observer;
30592
30588
  }
30589
+ var RECOMPUTE_INTERVAL_MS = 100;
30590
+ function rebuildDue(dirty, lastAt, now2) {
30591
+ return dirty && now2 - lastAt >= RECOMPUTE_INTERVAL_MS;
30592
+ }
30593
30593
  function startOffCanvasIndicatorRefresh(options) {
30594
30594
  let frame = 0;
30595
30595
  let lastCompSig = "";
30596
+ let lastRecomputeAt = Number.NEGATIVE_INFINITY;
30596
30597
  const markDirty = () => {
30597
30598
  options.dirtyRef.current = true;
30598
30599
  };
@@ -30621,7 +30622,9 @@ function startOffCanvasIndicatorRefresh(options) {
30621
30622
  if (options.dirtyRef.current) clearIndicators(options);
30622
30623
  return;
30623
30624
  }
30624
- if (!options.dirtyRef.current) return;
30625
+ const now2 = performance.now();
30626
+ if (!rebuildDue(options.dirtyRef.current, lastRecomputeAt, now2)) return;
30627
+ lastRecomputeAt = now2;
30625
30628
  options.dirtyRef.current = false;
30626
30629
  recomputeOffCanvasIndicators(
30627
30630
  iframe,
@@ -35368,35 +35371,52 @@ function useElementLifecycleOps({
35368
35371
  commitDomEditPatchBatches,
35369
35372
  onElementDeleted
35370
35373
  }) {
35371
- const handleDomEditElementDelete = useCallback51(
35374
+ const handleDomEditElementsDelete = useCallback51(
35372
35375
  // fallow-ignore-next-line complexity
35373
- async (selection) => {
35376
+ async (selections) => {
35374
35377
  const pid = projectIdRef.current;
35375
35378
  if (!pid) return;
35376
- const label = selection.label || selection.id || selection.selector || selection.tagName;
35379
+ const [selection] = selections;
35380
+ if (!selection) return;
35381
+ const label = selections.length === 1 ? selection.label || selection.id || selection.selector || selection.tagName : `${selections.length} elements`;
35382
+ if (selections.length > 1) showToast(`Deleting ${label}...`, "info");
35377
35383
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
35384
+ const sameFile = selections.filter(
35385
+ (candidate) => (candidate.sourceFile || activeCompPath || "index.html") === targetPath
35386
+ );
35378
35387
  try {
35379
35388
  const originalContent = await readProjectFileContent(pid, targetPath);
35380
- const patchTarget = buildDomEditPatchTarget(selection);
35381
- if (!patchTarget.id && !patchTarget.selector && !patchTarget.hfId) {
35389
+ const patchTargets = sameFile.map((member) => buildDomEditPatchTarget(member));
35390
+ if (patchTargets.some((t) => !t.id && !t.selector && !t.hfId)) {
35382
35391
  throw new Error("Selected element has no patchable target");
35383
35392
  }
35384
- if (onTrySdkDelete && selection.hfId) {
35385
- const handled = await onTrySdkDelete(selection.hfId, originalContent, targetPath);
35386
- if (cutoverCommittedOrThrow(handled)) {
35393
+ const hfIds = sameFile.map((member) => member.hfId).filter((hfId) => Boolean(hfId));
35394
+ if (onTrySdkDelete && hfIds.length === sameFile.length) {
35395
+ let allHandled = true;
35396
+ for (const hfId of hfIds) {
35397
+ const handled = await onTrySdkDelete(hfId, originalContent, targetPath);
35398
+ if (!cutoverCommittedOrThrow(handled)) {
35399
+ allHandled = false;
35400
+ break;
35401
+ }
35402
+ }
35403
+ if (allHandled) {
35387
35404
  clearDomSelection();
35388
35405
  usePlayerStore.getState().setSelectedElementId(null);
35389
- showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
35406
+ showToast(
35407
+ `Deleted ${label}. Use Undo to restore ${sameFile.length === 1 ? "it" : "them"}.`,
35408
+ "info"
35409
+ );
35390
35410
  return;
35391
35411
  }
35392
35412
  }
35393
35413
  domEditSaveTimestampRef.current = Date.now();
35394
35414
  const removeResponse = await fetch(
35395
- `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
35415
+ `/api/projects/${pid}/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
35396
35416
  {
35397
35417
  method: "POST",
35398
35418
  headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
35399
- body: JSON.stringify({ target: patchTarget })
35419
+ body: JSON.stringify({ targets: patchTargets })
35400
35420
  }
35401
35421
  );
35402
35422
  if (!removeResponse.ok) {
@@ -35406,6 +35426,10 @@ function useElementLifecycleOps({
35406
35426
  );
35407
35427
  }
35408
35428
  const removeData = await removeResponse.json();
35429
+ if (!removeData.changed) {
35430
+ reloadPreview();
35431
+ throw new Error("Nothing to delete \u2014 the preview was out of date. Try again.");
35432
+ }
35409
35433
  const patchedContent = typeof removeData.content === "string" ? removeData.content : originalContent;
35410
35434
  await saveProjectFilesWithHistory({
35411
35435
  projectId: pid,
@@ -35423,8 +35447,11 @@ function useElementLifecycleOps({
35423
35447
  usePlayerStore.getState().setSelectedElementId(null);
35424
35448
  forceReloadSdkSession?.();
35425
35449
  reloadPreview();
35426
- onElementDeleted?.(selection);
35427
- showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
35450
+ for (const member of sameFile) onElementDeleted?.(member);
35451
+ showToast(
35452
+ `Deleted ${label}. Use Undo to restore ${sameFile.length === 1 ? "it" : "them"}.`,
35453
+ "info"
35454
+ );
35428
35455
  } catch (error) {
35429
35456
  const message = error instanceof Error ? error.message : "Failed to delete element";
35430
35457
  showToast(message);
@@ -35551,8 +35578,15 @@ function useElementLifecycleOps({
35551
35578
  },
35552
35579
  [commitDomEditPatchBatches, onReorderShadow]
35553
35580
  );
35581
+ const handleDomEditElementDelete = useCallback51(
35582
+ async (selection) => {
35583
+ await handleDomEditElementsDelete([selection]);
35584
+ },
35585
+ [handleDomEditElementsDelete]
35586
+ );
35554
35587
  return {
35555
35588
  handleDomEditElementDelete,
35589
+ handleDomEditElementsDelete,
35556
35590
  handleDomZIndexReorderCommit
35557
35591
  };
35558
35592
  }
@@ -37638,6 +37672,9 @@ function selectionIdsMatch(currentIds, selectedIds, currentAnchor, wantedAnchor)
37638
37672
  }
37639
37673
  return currentAnchor === wantedAnchor;
37640
37674
  }
37675
+ function anchorIsOutsideSelection(anchor2, selectedIds) {
37676
+ return anchor2 !== null && !selectedIds.includes(anchor2);
37677
+ }
37641
37678
  function useTimelineSelectionPreviewSync({
37642
37679
  selectedElementId,
37643
37680
  selectedElementIds,
@@ -37688,6 +37725,11 @@ function useTimelineSelectionPreviewSync({
37688
37725
  return;
37689
37726
  }
37690
37727
  let cancelled = false;
37728
+ const warnSelectionMissingOnce = () => {
37729
+ if (missingSelectionKeyRef.current === selectedKey) return;
37730
+ missingSelectionKeyRef.current = selectedKey;
37731
+ onSelectionNotFound();
37732
+ };
37691
37733
  const syncSelection = async () => {
37692
37734
  const selections = [];
37693
37735
  let resolvableCount = 0;
@@ -37700,9 +37742,9 @@ function useTimelineSelectionPreviewSync({
37700
37742
  }
37701
37743
  if (cancelled) return;
37702
37744
  if (selections.length < resolvableCount) {
37703
- if (missingSelectionKeyRef.current !== selectedKey) {
37704
- missingSelectionKeyRef.current = selectedKey;
37705
- onSelectionNotFound();
37745
+ warnSelectionMissingOnce();
37746
+ if (anchorIsOutsideSelection(currentAnchor, selectedIds)) {
37747
+ applyDomSelection(null, { revealPanel: false, announce: false });
37706
37748
  }
37707
37749
  return;
37708
37750
  }
@@ -39358,6 +39400,14 @@ import { resolveEditingSections } from "@hyperframes/core/editing";
39358
39400
 
39359
39401
  // src/components/editor/propertyPanelMediaSection.tsx
39360
39402
  import { useEffect as useEffect48, useState as useState47 } from "react";
39403
+ import {
39404
+ AUDIO_GAIN_FADER_MAX,
39405
+ AUDIO_GAIN_FADER_MIN,
39406
+ audioFaderPositionToGain,
39407
+ formatAudioGain,
39408
+ audioGainToFaderPosition,
39409
+ audioGainToText
39410
+ } from "@hyperframes/core/audio-gain";
39361
39411
  import { Fragment as Fragment25, jsx as jsx83, jsxs as jsxs61 } from "react/jsx-runtime";
39362
39412
  function MediaSection({
39363
39413
  projectDir,
@@ -39375,7 +39425,7 @@ function MediaSection({
39375
39425
  const isVisualMedia = isVideo || isImage;
39376
39426
  const el = element.element;
39377
39427
  const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
39378
- const volumePercent = Math.round(volume * 100);
39428
+ const volumeFaderPosition = audioGainToFaderPosition(volume);
39379
39429
  const mediaStart = Number.parseFloat(
39380
39430
  element.dataAttributes["media-start"] ?? element.dataAttributes["playback-start"] ?? "0"
39381
39431
  ) || 0;
@@ -39557,14 +39607,14 @@ function MediaSection({
39557
39607
  SliderControl,
39558
39608
  {
39559
39609
  trackName: "Volume",
39560
- value: volumePercent,
39561
- min: 0,
39562
- max: 100,
39610
+ value: volumeFaderPosition,
39611
+ min: AUDIO_GAIN_FADER_MIN,
39612
+ max: AUDIO_GAIN_FADER_MAX,
39563
39613
  step: 1,
39564
- displayValue: `${volumePercent}%`,
39565
- formatDisplayValue: (next) => `${Math.round(next)}%`,
39614
+ displayValue: audioGainToText(volume),
39615
+ formatDisplayValue: (next) => audioGainToText(audioFaderPositionToGain(next)),
39566
39616
  onCommit: (next) => {
39567
- void onSetAttribute("volume", formatNumericValue(next / 100));
39617
+ void onSetAttribute("volume", formatAudioGain(audioFaderPositionToGain(next)));
39568
39618
  }
39569
39619
  }
39570
39620
  )
@@ -51435,6 +51485,14 @@ function FlatToggle({
51435
51485
  }
51436
51486
 
51437
51487
  // src/components/editor/propertyPanelFlatMediaSection.tsx
51488
+ import {
51489
+ AUDIO_GAIN_FADER_MAX as AUDIO_GAIN_FADER_MAX2,
51490
+ AUDIO_GAIN_FADER_MIN as AUDIO_GAIN_FADER_MIN2,
51491
+ audioFaderPositionToGain as audioFaderPositionToGain2,
51492
+ formatAudioGain as formatAudioGain2,
51493
+ audioGainToFaderPosition as audioGainToFaderPosition2,
51494
+ audioGainToText as audioGainToText2
51495
+ } from "@hyperframes/core/audio-gain";
51438
51496
  import { Fragment as Fragment37, jsx as jsx131, jsxs as jsxs106 } from "react/jsx-runtime";
51439
51497
  function FlatMediaSection({
51440
51498
  projectDir,
@@ -51455,7 +51513,7 @@ function FlatMediaSection({
51455
51513
  const isVisualMedia = isVideo || isImage;
51456
51514
  const el = element.element;
51457
51515
  const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
51458
- const volumePercent = Math.round(volume * 100);
51516
+ const volumeFaderPosition = audioGainToFaderPosition2(volume);
51459
51517
  const mediaStart = Number.parseFloat(
51460
51518
  element.dataAttributes["media-start"] ?? element.dataAttributes["playback-start"] ?? "0"
51461
51519
  ) || 0;
@@ -51598,13 +51656,14 @@ function FlatMediaSection({
51598
51656
  FlatSlider,
51599
51657
  {
51600
51658
  label: "Volume",
51601
- value: volumePercent,
51602
- min: 0,
51603
- max: 100,
51604
- tier: volumePercent === 100 ? "default" : "explicitCustom",
51605
- displayValue: `${volumePercent}%`,
51659
+ value: volumeFaderPosition,
51660
+ min: AUDIO_GAIN_FADER_MIN2,
51661
+ max: AUDIO_GAIN_FADER_MAX2,
51662
+ tier: volume === 1 ? "default" : "explicitCustom",
51663
+ displayValue: audioGainToText2(volume),
51606
51664
  disabled: volumeAutomated,
51607
- onCommit: (next) => void onSetAttribute("volume", formatNumericValue(next / 100))
51665
+ centerTick: true,
51666
+ onCommit: (next) => void onSetAttribute("volume", formatAudioGain2(audioFaderPositionToGain2(next)))
51608
51667
  }
51609
51668
  ) }),
51610
51669
  /* @__PURE__ */ jsx131(
@@ -61820,36 +61879,44 @@ function useTimelineEditing({
61820
61879
  isRecordingRef,
61821
61880
  forceReloadSdkSession
61822
61881
  });
61823
- const handleTimelineElementDelete = useCallback91(
61882
+ const handleTimelineElementsDelete = useCallback91(
61824
61883
  // fallow-ignore-next-line complexity
61825
- async (element) => {
61884
+ async (selection) => {
61826
61885
  if (isRecordingRef?.current) {
61827
61886
  showToast("Cannot edit timeline while recording", "error");
61828
61887
  return;
61829
61888
  }
61830
61889
  const pid = projectIdRef.current;
61831
61890
  if (!pid) throw new Error("No active project");
61832
- const label = getTimelineElementLabel(element);
61891
+ const [element] = selection;
61892
+ if (!element) return;
61893
+ const label = selection.length === 1 ? getTimelineElementLabel(element) : `${selection.length} clips`;
61833
61894
  const targetPath = element.sourceFile || activeCompPath || "index.html";
61895
+ const sameFile = selection.filter(
61896
+ (candidate) => (candidate.sourceFile || activeCompPath || "index.html") === targetPath
61897
+ );
61834
61898
  try {
61835
61899
  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 })
61900
+ let removedContent = originalContent;
61901
+ for (const target of sameFile) {
61902
+ const patchTarget = buildPatchTarget(target);
61903
+ if (!patchTarget) {
61904
+ throw new Error(`Timeline element ${target.id} is missing a patchable target`);
61846
61905
  }
61847
- );
61848
- if (!removeResponse.ok) {
61849
- throw new Error(`Failed to delete ${element.id} from ${targetPath}`);
61906
+ const removeResponse = await fetch(
61907
+ `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
61908
+ {
61909
+ method: "POST",
61910
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
61911
+ body: JSON.stringify({ target: patchTarget })
61912
+ }
61913
+ );
61914
+ if (!removeResponse.ok) {
61915
+ throw new Error(`Failed to delete ${target.id} from ${targetPath}`);
61916
+ }
61917
+ const removeData = await removeResponse.json();
61918
+ if (typeof removeData.content === "string") removedContent = removeData.content;
61850
61919
  }
61851
- const removeData = await removeResponse.json();
61852
- const removedContent = typeof removeData.content === "string" ? removeData.content : originalContent;
61853
61920
  const deleteContentEnd = furthestClipEndFromSource(removedContent);
61854
61921
  const patchedContent = setCompositionDurationToContent(removedContent, deleteContentEnd);
61855
61922
  const rollbackDuration = captureDurationRollback(previewIframeRef.current);
@@ -61874,13 +61941,16 @@ function useTimelineEditing({
61874
61941
  rollbackDuration();
61875
61942
  throw error;
61876
61943
  }
61877
- usePlayerStore.getState().setElements(
61878
- timelineElements.filter((te) => (te.key ?? te.id) !== (element.key ?? element.id))
61879
- );
61944
+ const deletedKeys = new Set(sameFile.map((te) => te.key ?? te.id));
61945
+ usePlayerStore.getState().setElements(timelineElements.filter((te) => !deletedKeys.has(te.key ?? te.id)));
61880
61946
  usePlayerStore.getState().setSelectedElementId(null);
61947
+ usePlayerStore.getState().setSelectedElementIds(/* @__PURE__ */ new Set());
61881
61948
  forceReloadSdkSession?.();
61882
61949
  reloadPreview();
61883
- showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
61950
+ showToast(
61951
+ `Deleted ${label}. Use Undo to restore ${sameFile.length === 1 ? "it" : "them"}.`,
61952
+ "info"
61953
+ );
61884
61954
  } catch (error) {
61885
61955
  const message = error instanceof Error ? error.message : "Failed to delete timeline clip";
61886
61956
  showToast(message);
@@ -61899,6 +61969,12 @@ function useTimelineEditing({
61899
61969
  previewIframeRef
61900
61970
  ]
61901
61971
  );
61972
+ const handleTimelineElementDelete = useCallback91(
61973
+ async (element) => {
61974
+ await handleTimelineElementsDelete([element]);
61975
+ },
61976
+ [handleTimelineElementsDelete]
61977
+ );
61902
61978
  const { handleTimelineAssetDrop, handleTimelineFileDrop, handleTimelineCompositionDrop } = useTimelineAssetDropOps({
61903
61979
  projectIdRef,
61904
61980
  activeCompPath,
@@ -61940,6 +62016,7 @@ function useTimelineEditing({
61940
62016
  handleToggleTrackHidden,
61941
62017
  handleToggleElementHidden,
61942
62018
  handleTimelineElementDelete,
62019
+ handleTimelineElementsDelete,
61943
62020
  handleTimelineElementSplit: handleRazorSplit,
61944
62021
  handleRazorSplit,
61945
62022
  handleRazorSplitAll,
@@ -62341,7 +62418,7 @@ function useDomSelection({
62341
62418
  domEditGroupSelectionsRef.current = [];
62342
62419
  setDomEditSelection(null);
62343
62420
  setDomEditGroupSelections([]);
62344
- announceTimelineSelection2([], null);
62421
+ if (options?.announce !== false) announceTimelineSelection2([], null);
62345
62422
  return;
62346
62423
  }
62347
62424
  const isAdditiveSelection = Boolean(options?.additive);
@@ -62487,7 +62564,11 @@ function useDomSelection({
62487
62564
  }
62488
62565
  const selection = await buildDomSelectionForTimelineElement(element);
62489
62566
  if (seq !== timelineSelectSeqRef.current) return;
62490
- if (selection) applyDomSelection(selection);
62567
+ if (selection) {
62568
+ applyDomSelection(selection);
62569
+ return;
62570
+ }
62571
+ applyDomSelection(null, { revealPanel: false, announce: false });
62491
62572
  },
62492
62573
  [applyDomSelection, buildDomSelectionForTimelineElement]
62493
62574
  );
@@ -64463,7 +64544,7 @@ function useDomEditCommits({
64463
64544
  showToast,
64464
64545
  commitPositionPatchToHtml
64465
64546
  });
64466
- const { handleDomEditElementDelete, handleDomZIndexReorderCommit } = useElementLifecycleOps({
64547
+ const { handleDomEditElementDelete, handleDomEditElementsDelete, handleDomZIndexReorderCommit } = useElementLifecycleOps({
64467
64548
  activeCompPath,
64468
64549
  showToast,
64469
64550
  writeProjectFile,
@@ -64496,6 +64577,7 @@ function useDomEditCommits({
64496
64577
  handleDomRotationCommit,
64497
64578
  handleDomManualEditsReset,
64498
64579
  handleDomEditElementDelete,
64580
+ handleDomEditElementsDelete,
64499
64581
  handleDomZIndexReorderCommit
64500
64582
  };
64501
64583
  }
@@ -66051,7 +66133,7 @@ function useDomEditPreviewSync({
66051
66133
  refreshDomEditGroupSelectionsFromPreview,
66052
66134
  buildDomSelectionFromTarget,
66053
66135
  refreshPreviewDocumentVersion,
66054
- syncPreviewHistoryHotkey,
66136
+ syncPreviewHotkeys,
66055
66137
  applyStudioManualEditsToPreviewRef,
66056
66138
  openSourceForSelection,
66057
66139
  getSidebarTab,
@@ -66090,12 +66172,12 @@ function useDomEditPreviewSync({
66090
66172
  applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
66091
66173
  }
66092
66174
  };
66093
- syncPreviewHistoryHotkey(previewIframe);
66175
+ syncPreviewHotkeys(previewIframe);
66094
66176
  void applyStudioManualEditsToPreviewRef.current(previewIframe);
66095
66177
  void syncSelectionFromDocument();
66096
66178
  refreshPreviewDocumentVersion();
66097
66179
  const handleLoad = () => {
66098
- syncPreviewHistoryHotkey(previewIframe);
66180
+ syncPreviewHotkeys(previewIframe);
66099
66181
  void applyStudioManualEditsToPreviewRef.current(previewIframe);
66100
66182
  void syncSelectionFromDocument();
66101
66183
  refreshPreviewDocumentVersion();
@@ -66114,7 +66196,7 @@ function useDomEditPreviewSync({
66114
66196
  previewIframe,
66115
66197
  refreshDomEditGroupSelectionsFromPreview,
66116
66198
  refreshPreviewDocumentVersion,
66117
- syncPreviewHistoryHotkey,
66199
+ syncPreviewHotkeys,
66118
66200
  applyStudioManualEditsToPreviewRef,
66119
66201
  gsapCacheVersion
66120
66202
  ]);
@@ -66522,7 +66604,7 @@ function useDomEditWiring({
66522
66604
  bumpGsapCache,
66523
66605
  showToast,
66524
66606
  refreshPreviewDocumentVersion,
66525
- syncPreviewHistoryHotkey,
66607
+ syncPreviewHotkeys,
66526
66608
  applyStudioManualEditsToPreviewRef,
66527
66609
  applyDomSelection,
66528
66610
  buildDomSelectionFromTarget,
@@ -66633,7 +66715,7 @@ function useDomEditWiring({
66633
66715
  refreshDomEditGroupSelectionsFromPreview,
66634
66716
  buildDomSelectionFromTarget,
66635
66717
  refreshPreviewDocumentVersion,
66636
- syncPreviewHistoryHotkey,
66718
+ syncPreviewHotkeys,
66637
66719
  applyStudioManualEditsToPreviewRef,
66638
66720
  openSourceForSelection,
66639
66721
  getSidebarTab,
@@ -67849,6 +67931,9 @@ function useKeyframeEaseCommits({
67849
67931
  }
67850
67932
 
67851
67933
  // src/hooks/useDomEditSession.ts
67934
+ function membersForDelete(selection, group, options) {
67935
+ return options?.expandGroup && group.length > 0 ? group : [selection];
67936
+ }
67852
67937
  function useDomEditSession({
67853
67938
  projectId,
67854
67939
  activeCompPath,
@@ -67863,6 +67948,7 @@ function useDomEditSession({
67863
67948
  setRightCollapsed,
67864
67949
  setRightPanelTab,
67865
67950
  showToast,
67951
+ isRecordingRef,
67866
67952
  refreshPreviewDocumentVersion,
67867
67953
  queueDomEditSave,
67868
67954
  readProjectFile,
@@ -67879,7 +67965,7 @@ function useDomEditSession({
67879
67965
  previewDocumentVersion,
67880
67966
  rightPanelTab,
67881
67967
  applyStudioManualEditsToPreviewRef,
67882
- syncPreviewHistoryHotkey,
67968
+ syncPreviewHotkeys,
67883
67969
  reloadPreview,
67884
67970
  setRefreshKey: _setRefreshKey,
67885
67971
  openSourceForSelection,
@@ -68006,7 +68092,7 @@ function useDomEditSession({
68006
68092
  handleDomRemoveTextField,
68007
68093
  handleDomBoxSizeCommit,
68008
68094
  handleDomManualEditsReset,
68009
- handleDomEditElementDelete,
68095
+ handleDomEditElementsDelete,
68010
68096
  handleDomZIndexReorderCommit
68011
68097
  } = useDomEditCommits({
68012
68098
  activeCompPath,
@@ -68080,6 +68166,17 @@ function useDomEditSession({
68080
68166
  clearDomSelection,
68081
68167
  forceReloadSdkSession
68082
68168
  });
68169
+ const handleDomEditElementDelete = useCallback114(
68170
+ async (selection, options) => {
68171
+ if (isRecordingRef?.current) {
68172
+ showToast("Cannot edit timeline while recording", "error");
68173
+ return;
68174
+ }
68175
+ const members = membersForDelete(selection, domEditGroupSelectionsRef.current, options);
68176
+ await handleDomEditElementsDelete(members);
68177
+ },
68178
+ [domEditGroupSelectionsRef, handleDomEditElementsDelete, isRecordingRef, showToast]
68179
+ );
68083
68180
  const handleGroupSelection = useCallback114(() => {
68084
68181
  const group = domEditGroupSelectionsRef.current;
68085
68182
  const single = domEditSelectionRef.current;
@@ -68143,7 +68240,7 @@ function useDomEditSession({
68143
68240
  bumpGsapCache,
68144
68241
  showToast,
68145
68242
  refreshPreviewDocumentVersion,
68146
- syncPreviewHistoryHotkey,
68243
+ syncPreviewHotkeys,
68147
68244
  applyStudioManualEditsToPreviewRef,
68148
68245
  applyDomSelection,
68149
68246
  buildDomSelectionFromTarget,
@@ -69437,21 +69534,19 @@ function dispatchPlainKey(event, key2, cb) {
69437
69534
  return;
69438
69535
  }
69439
69536
  }
69537
+ const domSel = cb.domEditSelectionRef.current;
69538
+ if (domSel) {
69539
+ event.preventDefault();
69540
+ void cb.handleDomEditElementDelete(domSel, { expandGroup: true });
69541
+ return;
69542
+ }
69440
69543
  const { selectedElementId, selectedElementIds, elements } = usePlayerStore.getState();
69441
69544
  const selectionKeys = new Set(selectedElementIds);
69442
69545
  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) {
69546
+ const selected = elements.filter((e) => selectionKeys.has(e.key ?? e.id));
69547
+ if (selected.length > 0) {
69453
69548
  event.preventDefault();
69454
- void cb.handleDomEditElementDelete(domSel);
69549
+ void cb.handleTimelineElementsDelete(selected);
69455
69550
  }
69456
69551
  return;
69457
69552
  }
@@ -69461,7 +69556,7 @@ function dispatchPlainKey(event, key2, cb) {
69461
69556
  }
69462
69557
  }
69463
69558
  function useAppHotkeys({
69464
- handleTimelineElementDelete,
69559
+ handleTimelineElementsDelete,
69465
69560
  handleTimelineElementSplit,
69466
69561
  handleDomEditElementDelete,
69467
69562
  domEditSelectionRef,
@@ -69486,7 +69581,6 @@ function useAppHotkeys({
69486
69581
  activeCompPath,
69487
69582
  forceReloadSdkSession
69488
69583
  }) {
69489
- const previewHotkeyWindowRef = useRef126(null);
69490
69584
  const previewHistoryCleanupRef = useRef126(null);
69491
69585
  const readHistoryFile = useCallback119(
69492
69586
  (path) => path === STUDIO_MOTION_PATH ? readOptionalProjectFile(path) : readProjectFile(path),
@@ -69545,7 +69639,7 @@ function useAppHotkeys({
69545
69639
  const handleRedo = useCallback119(() => applyHistory("redo"), [applyHistory]);
69546
69640
  const cbRef = useRef126(null);
69547
69641
  cbRef.current = {
69548
- handleTimelineElementDelete,
69642
+ handleTimelineElementsDelete,
69549
69643
  handleTimelineElementSplit,
69550
69644
  handleDomEditElementDelete,
69551
69645
  handleUndo,
@@ -69575,31 +69669,6 @@ function useAppHotkeys({
69575
69669
  window.addEventListener("keydown", handleAppKeyDown, true);
69576
69670
  return () => window.removeEventListener("keydown", handleAppKeyDown, true);
69577
69671
  }, [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
69672
  const handleHistoryHotkey = useCallback119((event) => {
69604
69673
  if (!(event.metaKey || event.ctrlKey) || shouldIgnoreHistoryShortcut(event.target)) return;
69605
69674
  handleUndoRedoKey(
@@ -69608,7 +69677,7 @@ function useAppHotkeys({
69608
69677
  () => void cbRef.current.handleRedo()
69609
69678
  );
69610
69679
  }, []);
69611
- const syncPreviewHistoryHotkey = useCallback119(
69680
+ const syncPreviewHotkeys = useCallback119(
69612
69681
  (iframe) => {
69613
69682
  previewHistoryCleanupRef.current?.();
69614
69683
  previewHistoryCleanupRef.current = null;
@@ -69621,14 +69690,17 @@ function useAppHotkeys({
69621
69690
  }
69622
69691
  if (!win && !doc) return;
69623
69692
  const handler = handleHistoryHotkey;
69693
+ const appHandler = handleAppKeyDown;
69624
69694
  safeAddListener(win, "keydown", handler, true);
69695
+ safeAddListener(win, "keydown", appHandler, true);
69625
69696
  doc?.addEventListener("keydown", handleHistoryHotkey, true);
69626
69697
  previewHistoryCleanupRef.current = () => {
69627
69698
  safeRemoveListener(win, "keydown", handler);
69699
+ safeRemoveListener(win, "keydown", appHandler);
69628
69700
  doc?.removeEventListener("keydown", handleHistoryHotkey, true);
69629
69701
  };
69630
69702
  },
69631
- [handleHistoryHotkey]
69703
+ [handleAppKeyDown, handleHistoryHotkey]
69632
69704
  );
69633
69705
  useEffect99(
69634
69706
  () => () => {
@@ -69640,8 +69712,7 @@ function useAppHotkeys({
69640
69712
  return {
69641
69713
  handleUndo,
69642
69714
  handleRedo,
69643
- syncPreviewTimelineHotkey,
69644
- syncPreviewHistoryHotkey
69715
+ syncPreviewHotkeys
69645
69716
  };
69646
69717
  }
69647
69718
 
@@ -71207,7 +71278,7 @@ function findUrlSelectionElement(doc, target, fallbackSourceFile, activeCompPath
71207
71278
  }
71208
71279
  async function buildOptionalDomSelection(element, buildDomSelection) {
71209
71280
  if (!element) return null;
71210
- return buildDomSelection(element, { preferClipAncestor: false });
71281
+ return buildDomSelection(element, { preferClipAncestor: false, skipSourceProbe: true });
71211
71282
  }
71212
71283
  async function resolveUrlSelections({
71213
71284
  doc,
@@ -75568,6 +75639,7 @@ function getVisibleLayers(layers, collapsed) {
75568
75639
 
75569
75640
  // src/components/editor/LayersPanel.tsx
75570
75641
  import { jsx as jsx175, jsxs as jsxs145 } from "react/jsx-runtime";
75642
+ var LAYERS_PANEL_MAX_ROWS = 80;
75571
75643
  var TAG_ICONS = {
75572
75644
  video: "Vi",
75573
75645
  audio: "Au",
@@ -75652,11 +75724,13 @@ var LayersPanel = memo43(function LayersPanel2() {
75652
75724
  const root = doc.querySelector("[data-composition-id]") ?? doc.documentElement ?? null;
75653
75725
  if (!root) return;
75654
75726
  if (activeGroupElement && !activeGroupElement.isConnected) setActiveGroupElement(null);
75655
- const items = collectDomEditLayerItems(root, {
75656
- activeCompositionPath: activeCompPath,
75657
- isMasterView,
75658
- activeGroupElement
75659
- });
75727
+ const items = collectDomEditLayerItems(
75728
+ root,
75729
+ { activeCompositionPath: activeCompPath, isMasterView, activeGroupElement },
75730
+ // How many rows this panel is willing to render, nothing more. Hit-testing
75731
+ // callers deliberately take the whole document instead.
75732
+ LAYERS_PANEL_MAX_ROWS
75733
+ );
75660
75734
  setLayers(sortLayersByZIndex(items));
75661
75735
  }, [previewIframeRef, activeCompPath, isMasterView, activeGroupElement, setActiveGroupElement]);
75662
75736
  useEffect114(() => {
@@ -82607,11 +82681,9 @@ function StudioApp() {
82607
82681
  const clearDomSelectionRef = useRef157(() => {
82608
82682
  });
82609
82683
  const domEditSelectionBridgeRef = useRef157(null);
82610
- const handleDomEditElementDeleteRef = useRef157(
82611
- async () => {
82612
- }
82613
- );
82614
- const domEditDeleteBridge = (s) => handleDomEditElementDeleteRef.current(s);
82684
+ const handleDomEditElementDeleteRef = useRef157(async () => {
82685
+ });
82686
+ const domEditDeleteBridge = (s, o) => handleDomEditElementDeleteRef.current(s, o);
82615
82687
  const resetKeyframesRef = useRef157(() => false);
82616
82688
  const deleteSelectedKeyframesRef = useRef157(() => {
82617
82689
  });
@@ -82629,7 +82701,7 @@ function StudioApp() {
82629
82701
  previewIframeRef
82630
82702
  });
82631
82703
  const appHotkeys = useAppHotkeys({
82632
- handleTimelineElementDelete: timelineEditing.handleTimelineElementDelete,
82704
+ handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
82633
82705
  handleTimelineElementSplit: timelineEditing.handleTimelineElementSplit,
82634
82706
  handleDomEditElementDelete: domEditDeleteBridge,
82635
82707
  domEditSelectionRef: domEditSelectionBridgeRef,
@@ -82673,6 +82745,7 @@ function StudioApp() {
82673
82745
  setRightCollapsed: panelLayout.setRightCollapsed,
82674
82746
  setRightPanelTab: panelLayout.setRightPanelTab,
82675
82747
  showToast,
82748
+ isRecordingRef: isGestureRecordingRef,
82676
82749
  refreshPreviewDocumentVersion,
82677
82750
  queueDomEditSave: previewPersistence.queueDomEditSave,
82678
82751
  readProjectFile: fileManager.readProjectFile,
@@ -82689,7 +82762,7 @@ function StudioApp() {
82689
82762
  previewDocumentVersion,
82690
82763
  rightPanelTab: panelLayout.rightPanelTab,
82691
82764
  applyStudioManualEditsToPreviewRef: previewPersistence.applyStudioManualEditsToPreviewRef,
82692
- syncPreviewHistoryHotkey: appHotkeys.syncPreviewHistoryHotkey,
82765
+ syncPreviewHotkeys: appHotkeys.syncPreviewHotkeys,
82693
82766
  reloadPreview,
82694
82767
  setRefreshKey,
82695
82768
  openSourceForSelection: fileManager.openSourceForSelection,
@@ -82755,7 +82828,6 @@ function StudioApp() {
82755
82828
  isGestureRecordingRef
82756
82829
  });
82757
82830
  handleToggleRecordingRef.current = handleToggleRecording;
82758
- const recordingToggle = handleToggleRecording;
82759
82831
  const canvasRectRef = useRef157(null);
82760
82832
  useLayoutEffect9(() => {
82761
82833
  if (gestureState !== "recording" || !previewIframe) {
@@ -82768,8 +82840,7 @@ function StudioApp() {
82768
82840
  (iframe) => {
82769
82841
  previewIframeRef.current = iframe;
82770
82842
  setPreviewIframe(iframe);
82771
- appHotkeys.syncPreviewTimelineHotkey(iframe);
82772
- appHotkeys.syncPreviewHistoryHotkey(iframe);
82843
+ appHotkeys.syncPreviewHotkeys(iframe);
82773
82844
  resetConsoleErrors();
82774
82845
  refreshPreviewDocumentVersion();
82775
82846
  },
@@ -82920,7 +82991,7 @@ function StudioApp() {
82920
82991
  },
82921
82992
  recordingState: gestureState,
82922
82993
  recordingDuration: gestureRecording.recordingDuration,
82923
- onToggleRecording: recordingToggle,
82994
+ onToggleRecording: handleToggleRecording,
82924
82995
  sdkSession: sdkHandle.session,
82925
82996
  publishSdkSession: sdkHandle.publish,
82926
82997
  forceReloadSdkSession: sdkHandle.forceReload,
@@ -82954,7 +83025,7 @@ function StudioApp() {
82954
83025
  shouldShowSelectedDomBounds,
82955
83026
  isGestureRecording: gestureState === "recording",
82956
83027
  recordingState: gestureState,
82957
- onToggleRecording: recordingToggle,
83028
+ onToggleRecording: handleToggleRecording,
82958
83029
  blockPreview,
82959
83030
  gestureOverlay: gestureState === "recording" && previewIframe ? /* @__PURE__ */ jsx206(
82960
83031
  GestureTrailOverlay,