@hyperframes/studio 0.8.100 → 0.8.101

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 (39) hide show
  1. package/dist/assets/{hyperframes-player-fWt87gyh.js → hyperframes-player-CFJLrHRF.js} +1 -1
  2. package/dist/assets/{index-D6TjRAvz.js → index-BBNVnnyH.js} +114 -114
  3. package/dist/assets/{index-DofdzdxJ.js → index-C33EDBL2.js} +1 -1
  4. package/dist/assets/{index-D34Oy5DN.js → index-CJOzbslX.js} +1 -1
  5. package/dist/{chunk-U3EE4ACX.js → chunk-7X4XRP3L.js} +14 -13
  6. package/dist/chunk-7X4XRP3L.js.map +1 -0
  7. package/dist/index.d.ts +91 -21
  8. package/dist/index.html +1 -1
  9. package/dist/index.js +403 -226
  10. package/dist/index.js.map +1 -1
  11. package/dist/player.js +1 -1
  12. package/package.json +8 -8
  13. package/src/App.tsx +2 -0
  14. package/src/components/dock/Dock.test.tsx +78 -3
  15. package/src/components/dock/Dock.tsx +45 -18
  16. package/src/components/dock/dockLayout.ts +27 -14
  17. package/src/components/dock/dockLayoutSchema.ts +11 -4
  18. package/src/components/dock/dockLayoutStore.ts +4 -2
  19. package/src/hooks/appHotkeysDispatch.ts +1 -7
  20. package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +67 -2
  21. package/src/hooks/useAppHotkeys.ts +18 -45
  22. package/src/hooks/useEditHistoryActions.paint.test.tsx +166 -0
  23. package/src/hooks/useEditHistoryActions.test.tsx +67 -9
  24. package/src/hooks/useEditHistoryActions.ts +36 -8
  25. package/src/hooks/usePersistentEditHistory.test.ts +46 -0
  26. package/src/hooks/usePersistentEditHistory.ts +101 -19
  27. package/src/hooks/usePreviewPersistence.ts +25 -2
  28. package/src/hooks/useStudioExternalFileChanges.ts +7 -3
  29. package/src/index.ts +4 -0
  30. package/src/player/components/timelineDragDrop.test.tsx +61 -5
  31. package/src/player/components/timelineDragDrop.ts +29 -8
  32. package/src/player/components/useTimelineProviderState.tsx +1 -0
  33. package/src/utils/domEditSaveQueue.ts +7 -2
  34. package/src/utils/gsapSoftReload.ts +1 -1
  35. package/src/utils/gsapUndoRestore.test.ts +46 -0
  36. package/src/utils/gsapUndoRestore.ts +166 -91
  37. package/src/utils/studioPendingEdits.ts +14 -5
  38. package/src/utils/studioUiPreferences.ts +8 -6
  39. package/dist/chunk-U3EE4ACX.js.map +0 -1
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  isTimelineIgnoredElement,
44
44
  liveTime,
45
45
  panelsInZone,
46
+ parseDockLayout,
46
47
  playbackStartAttributeForElement,
47
48
  previewElementFinder,
48
49
  readLayerRevealPriorZ,
@@ -66,7 +67,7 @@ import {
66
67
  whenPreviewBooted,
67
68
  wrapTimeline,
68
69
  writeStudioUiPreferences
69
- } from "./chunk-U3EE4ACX.js";
70
+ } from "./chunk-7X4XRP3L.js";
70
71
  import {
71
72
  Button,
72
73
  ContextMenu,
@@ -3338,6 +3339,73 @@ function isLaneFree(elements, track2, start, end, excludeKey) {
3338
3339
  );
3339
3340
  }
3340
3341
 
3342
+ // src/player/components/timelineSnapping.ts
3343
+ function resolveSnapGuide(moving, trimming) {
3344
+ const source = moving?.started ? moving : trimming;
3345
+ if (source?.snapTime == null || source.snapType == null) return null;
3346
+ return { time: source.snapTime, type: source.snapType };
3347
+ }
3348
+ var TIMELINE_SNAP_PX = 8;
3349
+ var TYPE_PRIORITY = {
3350
+ playhead: 0,
3351
+ "clip-edge": 1,
3352
+ beat: 2
3353
+ };
3354
+ function collectTimelineSnapTargets(input) {
3355
+ const byTime = /* @__PURE__ */ new Map();
3356
+ const add = (time, type) => {
3357
+ if (!Number.isFinite(time) || time < 0) return;
3358
+ const rounded2 = Math.round(time * 1e3) / 1e3;
3359
+ const existing = byTime.get(rounded2);
3360
+ if (!existing || TYPE_PRIORITY[type] < TYPE_PRIORITY[existing.type]) {
3361
+ byTime.set(rounded2, { time: rounded2, type });
3362
+ }
3363
+ };
3364
+ for (const beat of input.beatTimes) add(beat, "beat");
3365
+ for (const el of input.elements) {
3366
+ if (input.excludeElementKey != null && (el.key ?? el.id) === input.excludeElementKey) continue;
3367
+ add(el.start, "clip-edge");
3368
+ add(el.start + el.duration, "clip-edge");
3369
+ }
3370
+ if (input.playheadTime != null && input.includePlayhead !== false)
3371
+ add(input.playheadTime, "playhead");
3372
+ return Array.from(byTime.values()).sort((a, b) => a.time - b.time);
3373
+ }
3374
+ function snapTimelineTime(time, targets, thresholdSecs) {
3375
+ let best = null;
3376
+ let bestDist = thresholdSecs;
3377
+ for (const target of targets) {
3378
+ const d = Math.abs(target.time - time);
3379
+ if (d < bestDist || d === bestDist && best && TYPE_PRIORITY[target.type] < TYPE_PRIORITY[best.type]) {
3380
+ bestDist = d;
3381
+ best = target;
3382
+ }
3383
+ }
3384
+ return best ? { time: best.time, target: best } : { time, target: null };
3385
+ }
3386
+ function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDuration2) {
3387
+ if (targets.length === 0) return { start, snapTime: null, snapType: null };
3388
+ const thresholdSecs = TIMELINE_SNAP_PX / Math.max(pixelsPerSecond, 1);
3389
+ const startSnap = snapTimelineTime(start, targets, thresholdSecs);
3390
+ const endSnap = snapTimelineTime(start + duration, targets, thresholdSecs);
3391
+ const startMoved = startSnap.target !== null;
3392
+ const endMoved = endSnap.target !== null;
3393
+ let candidate = start;
3394
+ let target = null;
3395
+ if (startMoved && (!endMoved || Math.abs(startSnap.time - start) <= Math.abs(endSnap.time - (start + duration)))) {
3396
+ candidate = startSnap.time;
3397
+ target = startSnap.target;
3398
+ } else if (endMoved) {
3399
+ candidate = endSnap.time - duration;
3400
+ target = endSnap.target;
3401
+ }
3402
+ const maxStart = Math.max(0, timelineDuration2 - duration);
3403
+ const roundedCandidate = Math.round(candidate * 1e3) / 1e3;
3404
+ const clamped = Math.max(0, Math.min(maxStart, roundedCandidate));
3405
+ if (target && Math.abs(clamped - roundedCandidate) > 1e-6) target = null;
3406
+ return { start: clamped, snapTime: target?.time ?? null, snapType: target?.type ?? null };
3407
+ }
3408
+
3341
3409
  // src/player/components/timelineDragDrop.ts
3342
3410
  function applyJsonDropPayload(raw, pick, apply, placement) {
3343
3411
  try {
@@ -3358,12 +3426,20 @@ function resolveDropInsertRow(contentY, rowHeights, trackCount) {
3358
3426
  if (rowFloat >= trackCount) return null;
3359
3427
  return resolveInsertRow(rowFloat, trackCount);
3360
3428
  }
3361
- function placeDrop(geometry, clientX, clientY, canInsertTrack) {
3429
+ function alignDropStart(placement, elements, pixelsPerSecond) {
3430
+ const laneHasClips = placement.insertRow == null && elements.some((el) => el.track === placement.track);
3431
+ if (!laneHasClips) return { ...placement, start: 0 };
3432
+ if (!usePlayerStore.getState().timelineSnapEnabled) return placement;
3433
+ const targets = collectTimelineSnapTargets({ elements, playheadTime: null, beatTimes: [] });
3434
+ const threshold = TIMELINE_SNAP_PX / Math.max(pixelsPerSecond, 1);
3435
+ return { ...placement, start: snapTimelineTime(placement.start, targets, threshold).time };
3436
+ }
3437
+ function placeDrop(geometry, elements, clientX, clientY, canInsertTrack) {
3362
3438
  const placement = resolveTimelineAssetDrop(geometry, clientX, clientY);
3363
- if (!canInsertTrack) return placement;
3364
3439
  const contentY = clientY - geometry.rectTop + geometry.scrollTop;
3365
- const insertRow = resolveDropInsertRow(contentY, geometry.rowHeights, geometry.trackOrder.length);
3366
- return insertRow == null ? placement : { ...placement, insertRow, trackOrder: geometry.trackOrder };
3440
+ const insertRow = canInsertTrack ? resolveDropInsertRow(contentY, geometry.rowHeights, geometry.trackOrder.length) : null;
3441
+ const aimed = insertRow == null ? placement : { ...placement, insertRow, trackOrder: geometry.trackOrder };
3442
+ return alignDropStart(aimed, elements, geometry.pixelsPerSecond);
3367
3443
  }
3368
3444
  function invokeDropCallback(callback) {
3369
3445
  try {
@@ -3391,6 +3467,7 @@ function useTimelineAssetDrop({
3391
3467
  scrollRef,
3392
3468
  ppsRef,
3393
3469
  trackOrderRef,
3470
+ elementsRef,
3394
3471
  rowGeometryRef,
3395
3472
  contentOrigin,
3396
3473
  onFileDrop,
@@ -3458,12 +3535,13 @@ function useTimelineAssetDrop({
3458
3535
  rowHeights: rowGeometryRef.current.rowHeights,
3459
3536
  trackOrder: trackOrderRef.current
3460
3537
  },
3538
+ elementsRef.current,
3461
3539
  clientX,
3462
3540
  clientY,
3463
3541
  canInsertTrack
3464
3542
  );
3465
3543
  },
3466
- [scrollRef, ppsRef, trackOrderRef, rowGeometryRef, contentOrigin]
3544
+ [scrollRef, ppsRef, trackOrderRef, elementsRef, rowGeometryRef, contentOrigin]
3467
3545
  );
3468
3546
  const handleAssetDragOver = useCallback6(
3469
3547
  (e) => {
@@ -3558,73 +3636,6 @@ function useTimelineAssetDrop({
3558
3636
  // src/player/components/useTimelineClipDrag.ts
3559
3637
  import { useRef as useRef7, useState as useState4, useCallback as useCallback7, useMemo as useMemo3, useEffect as useEffect7 } from "react";
3560
3638
 
3561
- // src/player/components/timelineSnapping.ts
3562
- function resolveSnapGuide(moving, trimming) {
3563
- const source = moving?.started ? moving : trimming;
3564
- if (source?.snapTime == null || source.snapType == null) return null;
3565
- return { time: source.snapTime, type: source.snapType };
3566
- }
3567
- var TIMELINE_SNAP_PX = 8;
3568
- var TYPE_PRIORITY = {
3569
- playhead: 0,
3570
- "clip-edge": 1,
3571
- beat: 2
3572
- };
3573
- function collectTimelineSnapTargets(input) {
3574
- const byTime = /* @__PURE__ */ new Map();
3575
- const add = (time, type) => {
3576
- if (!Number.isFinite(time) || time < 0) return;
3577
- const rounded2 = Math.round(time * 1e3) / 1e3;
3578
- const existing = byTime.get(rounded2);
3579
- if (!existing || TYPE_PRIORITY[type] < TYPE_PRIORITY[existing.type]) {
3580
- byTime.set(rounded2, { time: rounded2, type });
3581
- }
3582
- };
3583
- for (const beat of input.beatTimes) add(beat, "beat");
3584
- for (const el of input.elements) {
3585
- if (input.excludeElementKey != null && (el.key ?? el.id) === input.excludeElementKey) continue;
3586
- add(el.start, "clip-edge");
3587
- add(el.start + el.duration, "clip-edge");
3588
- }
3589
- if (input.playheadTime != null && input.includePlayhead !== false)
3590
- add(input.playheadTime, "playhead");
3591
- return Array.from(byTime.values()).sort((a, b) => a.time - b.time);
3592
- }
3593
- function snapTimelineTime(time, targets, thresholdSecs) {
3594
- let best = null;
3595
- let bestDist = thresholdSecs;
3596
- for (const target of targets) {
3597
- const d = Math.abs(target.time - time);
3598
- if (d < bestDist || d === bestDist && best && TYPE_PRIORITY[target.type] < TYPE_PRIORITY[best.type]) {
3599
- bestDist = d;
3600
- best = target;
3601
- }
3602
- }
3603
- return best ? { time: best.time, target: best } : { time, target: null };
3604
- }
3605
- function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDuration2) {
3606
- if (targets.length === 0) return { start, snapTime: null, snapType: null };
3607
- const thresholdSecs = TIMELINE_SNAP_PX / Math.max(pixelsPerSecond, 1);
3608
- const startSnap = snapTimelineTime(start, targets, thresholdSecs);
3609
- const endSnap = snapTimelineTime(start + duration, targets, thresholdSecs);
3610
- const startMoved = startSnap.target !== null;
3611
- const endMoved = endSnap.target !== null;
3612
- let candidate = start;
3613
- let target = null;
3614
- if (startMoved && (!endMoved || Math.abs(startSnap.time - start) <= Math.abs(endSnap.time - (start + duration)))) {
3615
- candidate = startSnap.time;
3616
- target = startSnap.target;
3617
- } else if (endMoved) {
3618
- candidate = endSnap.time - duration;
3619
- target = endSnap.target;
3620
- }
3621
- const maxStart = Math.max(0, timelineDuration2 - duration);
3622
- const roundedCandidate = Math.round(candidate * 1e3) / 1e3;
3623
- const clamped = Math.max(0, Math.min(maxStart, roundedCandidate));
3624
- if (target && Math.abs(clamped - roundedCandidate) > 1e-6) target = null;
3625
- return { start: clamped, snapTime: target?.time ?? null, snapType: target?.type ?? null };
3626
- }
3627
-
3628
3639
  // src/player/components/timelineAuthoredMoveTarget.ts
3629
3640
  function canMoveTimelineElement(element) {
3630
3641
  return getTimelineEditCapabilities({
@@ -13033,6 +13044,7 @@ function useTimelineProviderState({
13033
13044
  scrollRef,
13034
13045
  ppsRef,
13035
13046
  trackOrderRef,
13047
+ elementsRef: timelineElementsRef,
13036
13048
  rowGeometryRef,
13037
13049
  contentOrigin,
13038
13050
  onFileDrop: pinnedOnFileDrop,
@@ -33955,6 +33967,7 @@ var DomEditSaveQueueOpenError = class extends Error {
33955
33967
  function createDomEditSaveQueue(options = {}) {
33956
33968
  const failureThreshold = options.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
33957
33969
  let tail = Promise.resolve();
33970
+ let queuedCount = 0;
33958
33971
  let consecutiveFailures = 0;
33959
33972
  let breakerOpen = false;
33960
33973
  let drainError = null;
@@ -33993,13 +34006,14 @@ function createDomEditSaveQueue(options = {}) {
33993
34006
  return {
33994
34007
  enqueue(save) {
33995
34008
  if (breakerOpen) return Promise.reject(new DomEditSaveQueueOpenError());
34009
+ queuedCount += 1;
33996
34010
  const queued = tail.catch(() => void 0).then(() => {
33997
34011
  if (breakerOpen) throw new DomEditSaveQueueOpenError();
33998
34012
  return run(save);
33999
34013
  });
34000
34014
  tail = queued.then(
34001
- () => void 0,
34002
- () => void 0
34015
+ () => void (queuedCount -= 1),
34016
+ () => void (queuedCount -= 1)
34003
34017
  );
34004
34018
  return queued;
34005
34019
  },
@@ -34011,6 +34025,7 @@ function createDomEditSaveQueue(options = {}) {
34011
34025
  if (drainError != null) return { status: "failed", error: drainError };
34012
34026
  return { status: "clean" };
34013
34027
  },
34028
+ isIdle: () => queuedCount === 0,
34014
34029
  reset,
34015
34030
  destroy() {
34016
34031
  reset(false);
@@ -45120,12 +45135,13 @@ import { CornersIn, CornersOut, DotsThree, X as X2 } from "@phosphor-icons/react
45120
45135
  import { create as create6 } from "zustand";
45121
45136
  var useDockLayoutStore = create6((set, get) => ({
45122
45137
  controller: null,
45138
+ panels: PANEL_IDS,
45123
45139
  openPanels: new Set(PANEL_IDS),
45124
45140
  visiblePanels: new Set(PANEL_IDS),
45125
45141
  activePanel: null,
45126
45142
  lastActive: {},
45127
45143
  pendingActivation: null,
45128
- attach: (controller) => set({ controller }),
45144
+ attach: (controller, panels = PANEL_IDS) => set({ controller, panels }),
45129
45145
  detach: () => set({ controller: null, lastActive: {} }),
45130
45146
  sync: (snapshot3) => set((state) => {
45131
45147
  const lastActive = { ...state.lastActive };
@@ -45401,6 +45417,8 @@ var MIN_TIMELINE_H = 100;
45401
45417
  var MIN_SIDE_W = 200;
45402
45418
  var MIN_SIDE_W_FLOOR = 120;
45403
45419
  var DEFAULT_TIMELINE_H = 360;
45420
+ var DEFAULT_LEFT = ["compositions", "assets", "code", "catalog"];
45421
+ var DEFAULT_RIGHT = ["design", "layers", "renders", "variables"];
45404
45422
  function defaultSideWidths(viewportWidth) {
45405
45423
  const floor = sideMinimumWidth(viewportWidth);
45406
45424
  const left = Math.max(floor, Math.min(384, Math.round(viewportWidth * 0.257)));
@@ -45456,24 +45474,25 @@ function addRegisteredPanel(api, id, position) {
45456
45474
  ...position ? { position } : {}
45457
45475
  });
45458
45476
  }
45459
- function buildEditLayout(api, viewportWidth) {
45477
+ function addSideColumn(api, ids, panels, direction) {
45478
+ const [first, ...rest] = ids.filter((id) => panels.includes(id));
45479
+ if (!first) return void 0;
45480
+ addRegisteredPanel(api, first, { referencePanel: "preview", direction });
45481
+ for (const id of rest)
45482
+ addRegisteredPanel(api, id, { referencePanel: first, direction: "within" });
45483
+ api.getPanel(first)?.api.setActive();
45484
+ return first;
45485
+ }
45486
+ function buildEditLayout(api, viewportWidth, panels = PANEL_IDS) {
45460
45487
  api.clear();
45461
45488
  const widths = defaultSideWidths(viewportWidth);
45462
45489
  addRegisteredPanel(api, "preview");
45463
45490
  addRegisteredPanel(api, "timeline", { referencePanel: "preview", direction: "below" });
45464
- addRegisteredPanel(api, "compositions", { referencePanel: "preview", direction: "left" });
45465
- for (const id of ["assets", "code", "catalog"]) {
45466
- addRegisteredPanel(api, id, { referencePanel: "compositions", direction: "within" });
45467
- }
45468
- addRegisteredPanel(api, "design", { referencePanel: "preview", direction: "right" });
45469
- for (const id of ["layers", "renders", "variables"]) {
45470
- addRegisteredPanel(api, id, { referencePanel: "design", direction: "within" });
45471
- }
45472
- api.getPanel("compositions")?.api.setActive();
45473
- api.getPanel("design")?.api.setActive();
45491
+ const left = addSideColumn(api, DEFAULT_LEFT, panels, "left");
45492
+ const right = addSideColumn(api, DEFAULT_RIGHT, panels, "right");
45474
45493
  applySideMinimums(api, viewportWidth);
45475
- api.getPanel("compositions")?.group.api.setSize({ width: widths.left });
45476
- api.getPanel("design")?.group.api.setSize({ width: widths.right });
45494
+ if (left) api.getPanel(left)?.group.api.setSize({ width: widths.left });
45495
+ if (right) api.getPanel(right)?.group.api.setSize({ width: widths.right });
45477
45496
  api.getPanel("timeline")?.group.api.setSize({ height: DEFAULT_TIMELINE_H });
45478
45497
  }
45479
45498
 
@@ -45518,10 +45537,11 @@ function snapshot(api) {
45518
45537
  groupActivePanels
45519
45538
  };
45520
45539
  }
45521
- function createController(api) {
45540
+ function createController(api, options) {
45522
45541
  const open = (id) => {
45523
45542
  if (api.getPanel(id)) return;
45524
- const { zone, reopen } = PANEL_DEFINITIONS[id];
45543
+ const { zone, reopen: preferred } = PANEL_DEFINITIONS[id];
45544
+ const reopen = zone === "center" || options.panels.includes(preferred.near) ? preferred : { near: "preview", direction: zone };
45525
45545
  const sibling = zone === "center" ? void 0 : panelsInZone(zone).find((other) => other !== id && api.getPanel(other));
45526
45546
  if (sibling) {
45527
45547
  addRegisteredPanel(api, id, { referencePanel: sibling, direction: "within" });
@@ -45543,11 +45563,12 @@ function createController(api) {
45543
45563
  },
45544
45564
  setTitle: (id, title) => api.getPanel(id)?.api.setTitle(title),
45545
45565
  setGroupVisible: (id, visible) => api.getPanel(id)?.group.api.setVisible(visible),
45546
- reset: () => buildEditLayout(api, window.innerWidth)
45566
+ reset: () => buildEditLayout(api, options.dockWidth(), options.panels)
45547
45567
  };
45548
45568
  }
45549
- function restoreOrBuild(api, projectId) {
45550
- const stored = readStudioUiPreferences(void 0, projectId).dockLayout;
45569
+ function restoreOrBuild(api, projectId, options) {
45570
+ const { dockLayout } = readStudioUiPreferences(void 0, projectId, options.storageKey);
45571
+ const stored = parseDockLayout(dockLayout, options.panels);
45551
45572
  if (stored) {
45552
45573
  try {
45553
45574
  api.fromJSON(stored);
@@ -45555,9 +45576,16 @@ function restoreOrBuild(api, projectId) {
45555
45576
  } catch {
45556
45577
  }
45557
45578
  }
45558
- buildEditLayout(api, window.innerWidth);
45579
+ buildEditLayout(api, options.dockWidth(), options.panels);
45559
45580
  }
45560
- function Root({ projectId, children }) {
45581
+ var windowWidth = () => window.innerWidth;
45582
+ function Root({
45583
+ projectId,
45584
+ panels = PANEL_IDS,
45585
+ storageKey,
45586
+ dockWidth = windowWidth,
45587
+ children
45588
+ }) {
45561
45589
  const [slots, setSlots] = useState50({});
45562
45590
  const registerSlot = useCallback70((id, element) => {
45563
45591
  setSlots((prev) => {
@@ -45574,17 +45602,18 @@ function Root({ projectId, children }) {
45574
45602
  const onReady = useCallback70(
45575
45603
  ({ api }) => {
45576
45604
  disposeRef.current();
45577
- restoreOrBuild(api, projectId);
45578
- applySideMinimums(api);
45605
+ const options = { panels, storageKey, dockWidth };
45606
+ restoreOrBuild(api, projectId, options);
45607
+ applySideMinimums(api, dockWidth());
45579
45608
  const root = api.groups[0]?.element.closest(".hf-dock");
45580
45609
  const disposeAccessibility = root ? installDockAccessibility(api, root) : () => {
45581
45610
  };
45582
45611
  const disposeTabFill = root ? installTabFill(api, root) : () => {
45583
45612
  };
45584
- const resizeObserver = new ResizeObserver(() => applySideMinimums(api, window.innerWidth));
45613
+ const resizeObserver = new ResizeObserver(() => applySideMinimums(api, dockWidth()));
45585
45614
  if (root) resizeObserver.observe(root);
45586
45615
  const store = useDockLayoutStore.getState();
45587
- store.attach(createController(api));
45616
+ store.attach(createController(api, options), panels);
45588
45617
  store.sync(snapshot(api));
45589
45618
  const pending2 = store.takePendingActivation();
45590
45619
  if (pending2) api.getPanel(pending2)?.api.setActive();
@@ -45592,7 +45621,7 @@ function Root({ projectId, children }) {
45592
45621
  const persist2 = () => {
45593
45622
  clearTimeout(timer);
45594
45623
  timer = setTimeout(() => {
45595
- writeStudioUiPreferences({ dockLayout: api.toJSON() }, void 0, projectId);
45624
+ writeStudioUiPreferences({ dockLayout: api.toJSON() }, void 0, projectId, storageKey);
45596
45625
  }, PERSIST_DEBOUNCE_MS2);
45597
45626
  };
45598
45627
  const onDockChange = () => {
@@ -45601,11 +45630,11 @@ function Root({ projectId, children }) {
45601
45630
  };
45602
45631
  const subscriptions = [
45603
45632
  api.onDidAddPanel(() => {
45604
- applySideMinimums(api);
45633
+ applySideMinimums(api, dockWidth());
45605
45634
  onDockChange();
45606
45635
  }),
45607
45636
  api.onDidMovePanel(() => {
45608
- applySideMinimums(api);
45637
+ applySideMinimums(api, dockWidth());
45609
45638
  onDockChange();
45610
45639
  }),
45611
45640
  api.onDidRemovePanel(onDockChange),
@@ -45621,7 +45650,7 @@ function Root({ projectId, children }) {
45621
45650
  useDockLayoutStore.getState().detach();
45622
45651
  };
45623
45652
  },
45624
- [projectId]
45653
+ [projectId, panels, storageKey, dockWidth]
45625
45654
  );
45626
45655
  return /* @__PURE__ */ jsxs71(SlotsContext.Provider, { value: { slots, registerSlot }, children: [
45627
45656
  /* @__PURE__ */ jsx101(
@@ -45656,6 +45685,7 @@ function Panel({ id, title, children }) {
45656
45685
  }
45657
45686
  function WindowMenu() {
45658
45687
  const openPanels = useDockLayoutStore((state) => state.openPanels);
45688
+ const panels = useDockLayoutStore((state) => state.panels);
45659
45689
  const togglePanel = useDockLayoutStore((state) => state.togglePanel);
45660
45690
  const resetLayout = useDockLayoutStore((state) => state.resetLayout);
45661
45691
  const [open, setOpen] = useState50(false);
@@ -45686,7 +45716,7 @@ function WindowMenu() {
45686
45716
  role: "menu",
45687
45717
  className: "absolute right-0 top-8 z-50 w-44 rounded-md border border-neutral-800 bg-neutral-900 py-1 shadow-lg",
45688
45718
  children: [
45689
- PANEL_IDS.map((id) => /* @__PURE__ */ jsxs71(
45719
+ panels.map((id) => /* @__PURE__ */ jsxs71(
45690
45720
  "button",
45691
45721
  {
45692
45722
  type: "button",
@@ -49064,6 +49094,13 @@ function inspectDrainFailures(results) {
49064
49094
  }
49065
49095
  return { firstFailure };
49066
49096
  }
49097
+ function focusedField() {
49098
+ const active = document.activeElement;
49099
+ return active instanceof HTMLElement && active.matches('input, textarea, select, [contenteditable="true"], [role="textbox"]') ? active : null;
49100
+ }
49101
+ function hasStudioPendingEdits() {
49102
+ return pendingEditPromises.size > 0 || focusedField() !== null;
49103
+ }
49067
49104
  function trackStudioPendingEdit(result) {
49068
49105
  if (!result) return void 0;
49069
49106
  const promise = Promise.resolve(result);
@@ -49075,8 +49112,8 @@ function trackStudioPendingEdit(result) {
49075
49112
  return promise;
49076
49113
  }
49077
49114
  async function flushStudioPendingEdits() {
49078
- const active = document.activeElement;
49079
- if (active instanceof HTMLElement && active.matches('input, textarea, select, [contenteditable="true"], [role="textbox"]')) {
49115
+ const active = focusedField();
49116
+ if (active) {
49080
49117
  active.blur();
49081
49118
  await Promise.resolve();
49082
49119
  await waitForPostBlurEffects();
@@ -68103,6 +68140,60 @@ function useCaptionSync(projectId) {
68103
68140
  // src/hooks/usePersistentEditHistory.ts
68104
68141
  import { useCallback as useCallback97, useEffect as useEffect105, useMemo as useMemo65, useRef as useRef124, useState as useState104 } from "react";
68105
68142
  var EMPTY3 = { entries: [], back: null, forward: null };
68143
+ var OWN_ENTRIES_KEPT = 100;
68144
+ function createOwnHistory() {
68145
+ const own = /* @__PURE__ */ new Map();
68146
+ let next = null;
68147
+ let changes = 0;
68148
+ const remember = (id, files) => {
68149
+ const known = own.get(id) ?? {};
68150
+ for (const [path, { before, after }] of Object.entries(files)) {
68151
+ known[path] = { before: known[path]?.before ?? before, after };
68152
+ }
68153
+ own.delete(id);
68154
+ own.set(id, known);
68155
+ if (own.size > OWN_ENTRIES_KEPT) own.delete(own.keys().next().value);
68156
+ };
68157
+ return {
68158
+ remember,
68159
+ overtake: () => {
68160
+ changes += 1;
68161
+ next = null;
68162
+ return changes;
68163
+ },
68164
+ offered: (seen, view) => {
68165
+ if (seen === changes) next = { undo: view.back, redo: view.forward };
68166
+ },
68167
+ changes: () => changes,
68168
+ stepped: (entry2) => {
68169
+ const undone = entry2.undoes ? own.get(entry2.undoes) : void 0;
68170
+ if (!undone) return;
68171
+ const swapped = Object.entries(undone).map(([path, f]) => [
68172
+ path,
68173
+ { before: f.after, after: f.before }
68174
+ ]);
68175
+ remember(entry2.id, Object.fromEntries(swapped));
68176
+ },
68177
+ afterOf: (id) => Object.fromEntries(Object.entries(id && own.get(id) || {}).map(([p, f]) => [p, f.after])),
68178
+ predict: (direction) => {
68179
+ const step = next?.[direction];
68180
+ const files = step ? own.get(step.id) : void 0;
68181
+ const paths = files ? Object.keys(files) : [];
68182
+ if (!files || paths.length !== step.paths.length || !paths.every((p) => step.paths.includes(p)))
68183
+ return null;
68184
+ const restore = paths.map((path) => [
68185
+ path,
68186
+ { previous: files[path].after, restored: files[path].before }
68187
+ ]);
68188
+ return { id: step.id, files: Object.fromEntries(restore) };
68189
+ },
68190
+ clear: () => {
68191
+ own.clear();
68192
+ next = null;
68193
+ changes += 1;
68194
+ }
68195
+ };
68196
+ }
68106
68197
  var DEFAULT_COALESCE_MS = 300;
68107
68198
  function historyUrl(projectId, path = "") {
68108
68199
  return `/api/projects/${encodeURIComponent(projectId)}/history${path}`;
@@ -68121,10 +68212,11 @@ async function post(url, body, headers = {}) {
68121
68212
  return { ok: false, status: response.status, error: reply?.error ?? `HTTP ${response.status}` };
68122
68213
  }
68123
68214
  function claimHeld(reply, label) {
68124
- if (reply.ok) return Boolean(reply.body?.claimed);
68215
+ if (reply.ok)
68216
+ return reply.body?.claimed?.id ?? null;
68125
68217
  if (reply.status !== 404)
68126
68218
  console.error(`"${label}" was not recorded as your edit: ${reply.error}`);
68127
- return false;
68219
+ return null;
68128
68220
  }
68129
68221
  async function overwroteVersions(files) {
68130
68222
  const pairs = await Promise.all(
@@ -68162,38 +68254,47 @@ function usePersistentEditHistory({ projectId }) {
68162
68254
  const heldClaimRef = useRef124(null);
68163
68255
  const projectIdRef = useRef124(projectId);
68164
68256
  projectIdRef.current = projectId;
68257
+ const [own] = useState104(createOwnHistory);
68165
68258
  const refresh = useCallback97(async () => {
68166
68259
  if (!projectId) return;
68260
+ const seen = own.changes();
68167
68261
  const response = await fetch(historyUrl(projectId)).catch(() => null);
68168
68262
  const next = response?.ok ? await response.json() : EMPTY3;
68169
- if (projectIdRef.current === projectId) setView(next);
68170
- }, [projectId]);
68263
+ if (projectIdRef.current !== projectId) return;
68264
+ setView(next);
68265
+ own.offered(seen, next);
68266
+ }, [projectId, own]);
68171
68267
  useEffect105(() => {
68172
68268
  setView(EMPTY3);
68173
68269
  setLoaded(false);
68174
68270
  heldClaimRef.current = null;
68271
+ own.clear();
68175
68272
  void refresh().finally(() => setLoaded(true));
68176
- }, [refresh]);
68273
+ }, [refresh, own]);
68177
68274
  const recordEdit = useCallback97(
68178
68275
  async ({ label, coalesceKey, coalesceMs, files }) => {
68179
68276
  if (!projectId) return;
68180
68277
  const paths = Object.keys(files);
68278
+ own.overtake();
68181
68279
  const reply = await post(historyUrl(projectId, "/claim"), {
68182
68280
  label,
68183
68281
  paths,
68184
68282
  overwrote: await overwroteVersions(files),
68185
68283
  ...coalesceKey && { coalesceKey, idleMs: coalesceMs ?? DEFAULT_COALESCE_MS }
68186
68284
  });
68187
- heldClaimRef.current = claimHeld(reply, label) && coalesceKey ? { paths, at: Date.now() } : null;
68285
+ const claimed = claimHeld(reply, label);
68286
+ if (claimed) own.remember(claimed, files);
68287
+ heldClaimRef.current = claimed && coalesceKey ? { paths, at: Date.now() } : null;
68188
68288
  void refresh();
68189
68289
  },
68190
- [projectId, refresh]
68290
+ [projectId, refresh, own]
68191
68291
  );
68192
68292
  const step = useCallback97(
68193
68293
  async (direction, callbacks) => {
68194
68294
  if (!projectId) return { ok: false, reason: "empty" };
68195
68295
  const next = direction === "undo" ? view.back : view.forward;
68196
68296
  const paths = [.../* @__PURE__ */ new Set([...next?.paths ?? [], ...heldClaimRef.current?.paths ?? []])];
68297
+ own.overtake();
68197
68298
  const run = async () => {
68198
68299
  const previous = await readAll(paths, callbacks.readFile);
68199
68300
  const posted = await post(
@@ -68211,18 +68312,28 @@ function usePersistentEditHistory({ projectId }) {
68211
68312
  return { ok: false, reason: "content-mismatch", paths: files };
68212
68313
  }
68213
68314
  if (!reply.entry) return { ok: false, reason: "empty" };
68315
+ own.stepped(reply.entry);
68214
68316
  const changed = reply.entry.files.map((file) => file.path);
68215
68317
  return {
68216
68318
  ok: true,
68217
68319
  label: reply.entry.label,
68320
+ undoes: reply.entry.undoes,
68218
68321
  paths: changed,
68219
- files: await restoredFiles(changed, previous, callbacks.readFile)
68322
+ files: await restoredFiles(
68323
+ changed,
68324
+ { ...own.afterOf(reply.entry.undoes), ...previous },
68325
+ callbacks.readFile
68326
+ )
68220
68327
  };
68221
68328
  };
68222
68329
  return callbacks.serialize ? callbacks.serialize(paths, run) : run();
68223
68330
  },
68224
- [projectId, view, refresh]
68331
+ [projectId, view, refresh, own]
68225
68332
  );
68333
+ const noteOutsideChange = useCallback97(() => {
68334
+ own.overtake();
68335
+ void refresh();
68336
+ }, [own, refresh]);
68226
68337
  const undo = useCallback97((callbacks) => step("undo", callbacks), [step]);
68227
68338
  const redo = useCallback97((callbacks) => step("redo", callbacks), [step]);
68228
68339
  const state = useMemo65(() => {
@@ -68244,7 +68355,9 @@ function usePersistentEditHistory({ projectId }) {
68244
68355
  state,
68245
68356
  recordEdit,
68246
68357
  undo,
68247
- redo
68358
+ redo,
68359
+ predict: own.predict,
68360
+ noteOutsideChange
68248
68361
  };
68249
68362
  }
68250
68363
 
@@ -69010,6 +69123,7 @@ function useFileManager({
69010
69123
  import { useCallback as useCallback103, useRef as useRef129, useState as useState107 } from "react";
69011
69124
 
69012
69125
  // src/utils/gsapUndoRestore.ts
69126
+ import { isCompositionTemplate as isCompositionTemplate2 } from "@hyperframes/parsers/hf-ids";
69013
69127
  function elementIdentityKey(el) {
69014
69128
  const hfId = el.getAttribute("data-hf-id");
69015
69129
  if (hfId) return `hf:${hfId}`;
@@ -69018,9 +69132,9 @@ function elementIdentityKey(el) {
69018
69132
  return null;
69019
69133
  }
69020
69134
  var IDENTITY_SELECTOR = "[id], [data-hf-id]";
69021
- function identityElementMap(doc) {
69135
+ function identityElementMap(root) {
69022
69136
  const map = /* @__PURE__ */ new Map();
69023
- for (const el of doc.querySelectorAll(IDENTITY_SELECTOR)) {
69137
+ for (const el of root.querySelectorAll(IDENTITY_SELECTOR)) {
69024
69138
  const key2 = elementIdentityKey(el);
69025
69139
  if (!key2) continue;
69026
69140
  if (map.has(key2)) return null;
@@ -69028,8 +69142,18 @@ function identityElementMap(doc) {
69028
69142
  }
69029
69143
  return map;
69030
69144
  }
69031
- function normalizeSoftResidual(doc) {
69032
- for (const el of doc.querySelectorAll(IDENTITY_SELECTOR)) {
69145
+ function parseRestoreSource(html2) {
69146
+ const doc = new DOMParser().parseFromString(html2, "text/html");
69147
+ for (const template of Array.from(doc.querySelectorAll("template")).filter(
69148
+ isCompositionTemplate2
69149
+ )) {
69150
+ template.replaceWith(template.content);
69151
+ }
69152
+ return doc;
69153
+ }
69154
+ function normalizeSoftResidual(root) {
69155
+ const self = root.matches(IDENTITY_SELECTOR) ? [root] : [];
69156
+ for (const el of [...self, ...root.querySelectorAll(IDENTITY_SELECTOR)]) {
69033
69157
  const id = el.getAttribute("id");
69034
69158
  const hfId = el.getAttribute("data-hf-id");
69035
69159
  for (const name of [...el.getAttributeNames()]) {
@@ -69038,7 +69162,7 @@ function normalizeSoftResidual(doc) {
69038
69162
  if (id) el.setAttribute("id", id);
69039
69163
  if (hfId) el.setAttribute("data-hf-id", hfId);
69040
69164
  }
69041
- for (const script of findGsapScriptElements(doc)) script.textContent = "";
69165
+ for (const script of findGsapScriptElements(root)) script.textContent = "";
69042
69166
  }
69043
69167
  function attributesEqual(a, b) {
69044
69168
  const aNames = a.getAttributeNames();
@@ -69048,15 +69172,7 @@ function attributesEqual(a, b) {
69048
69172
  }
69049
69173
  return true;
69050
69174
  }
69051
- function diffSoftReloadableRestore(previous, restored) {
69052
- let prevDoc;
69053
- let nextDoc;
69054
- try {
69055
- prevDoc = new DOMParser().parseFromString(previous, "text/html");
69056
- nextDoc = new DOMParser().parseFromString(restored, "text/html");
69057
- } catch {
69058
- return null;
69059
- }
69175
+ function diffRestoreDocs(prevDoc, nextDoc) {
69060
69176
  const prevByKey = identityElementMap(prevDoc);
69061
69177
  const nextByKey = identityElementMap(nextDoc);
69062
69178
  if (!prevByKey || !nextByKey) return null;
@@ -69067,10 +69183,12 @@ function diffSoftReloadableRestore(previous, restored) {
69067
69183
  if (!prevEl || prevEl.tagName !== nextEl.tagName) return null;
69068
69184
  if (!attributesEqual(prevEl, nextEl)) changedElementKeys.push(key2);
69069
69185
  }
69070
- normalizeSoftResidual(prevDoc);
69071
- normalizeSoftResidual(nextDoc);
69072
- if (prevDoc.documentElement.outerHTML !== nextDoc.documentElement.outerHTML) return null;
69073
- return { changedElementKeys };
69186
+ const [prevRest, nextRest] = [prevDoc, nextDoc].map((doc) => {
69187
+ const root = doc.documentElement.cloneNode(true);
69188
+ normalizeSoftResidual(root);
69189
+ return root.outerHTML;
69190
+ });
69191
+ return prevRest === nextRest ? changedElementKeys : null;
69074
69192
  }
69075
69193
  function syncElementAttributes(target, source) {
69076
69194
  for (const name of [...target.getAttributeNames()]) {
@@ -69089,16 +69207,84 @@ function syncStaleEditMarks(doc, restored) {
69089
69207
  }
69090
69208
  }
69091
69209
  }
69092
- function readGsapScriptTexts(html2) {
69093
- const doc = new DOMParser().parseFromString(html2, "text/html");
69094
- return findGsapScriptElements(doc).map((script) => script.textContent ?? "");
69095
- }
69096
- function hasAmbiguousGsapScriptChange(previous, restored) {
69097
- const previousScripts = readGsapScriptTexts(previous);
69098
- const restoredScripts = readGsapScriptTexts(restored);
69210
+ function hasAmbiguousGsapScriptChange(previousScripts, restoredScripts) {
69099
69211
  if (previousScripts.length <= 1 && restoredScripts.length <= 1) return false;
69100
69212
  return previousScripts.length !== restoredScripts.length || previousScripts.some((script, index) => script !== restoredScripts[index]);
69101
69213
  }
69214
+ function restoreScripted(prevDoc, nextDoc, isActive) {
69215
+ const [before, after] = [prevDoc, nextDoc].map(
69216
+ (doc) => findGsapScriptElements(doc).map((script) => script.textContent ?? "")
69217
+ );
69218
+ const scripted = before.length > 0 || after.length > 0;
69219
+ if (scripted && !isActive) return null;
69220
+ return hasAmbiguousGsapScriptChange(before, after) ? null : scripted;
69221
+ }
69222
+ function liveScopes(doc, path, isActive) {
69223
+ if (isActive) return [doc];
69224
+ return Array.from(doc.querySelectorAll(`[data-composition-file="${CSS.escape(path)}"]`));
69225
+ }
69226
+ function scopeTargets(scope, keys, restoredByKey) {
69227
+ const liveByKey = identityElementMap(scope);
69228
+ if (!liveByKey) return null;
69229
+ const targets = [];
69230
+ for (const key2 of keys) {
69231
+ const live = liveByKey.get(key2);
69232
+ const restored = restoredByKey.get(key2);
69233
+ if (!live || !restored || live.hasAttribute("data-hf-inner-root")) return null;
69234
+ targets.push({ live, restored });
69235
+ }
69236
+ return targets;
69237
+ }
69238
+ function fileTargets(doc, path, isActive, file) {
69239
+ const prevDoc = parseRestoreSource(file.previous);
69240
+ const nextDoc = parseRestoreSource(file.restored);
69241
+ const scripted = restoreScripted(prevDoc, nextDoc, isActive);
69242
+ if (scripted === null) return null;
69243
+ const keys = diffRestoreDocs(prevDoc, nextDoc);
69244
+ const restoredByKey = identityElementMap(nextDoc);
69245
+ if (!keys || !restoredByKey) return null;
69246
+ const found = liveScopes(doc, path, isActive).map(
69247
+ (scope) => scopeTargets(scope, keys, restoredByKey)
69248
+ );
69249
+ if (!found.length || found.includes(null)) return null;
69250
+ return { targets: found.flat(), scripted };
69251
+ }
69252
+ function planRestoreTargets(doc, activeDocPath, files) {
69253
+ const plan = { targets: [], scripted: false };
69254
+ for (const [path, file] of Object.entries(files)) {
69255
+ const found = fileTargets(doc, path, path === activeDocPath, file);
69256
+ if (!found) return null;
69257
+ plan.targets.push(...found.targets);
69258
+ plan.scripted ||= found.scripted;
69259
+ }
69260
+ return plan;
69261
+ }
69262
+ function finalizeInPlace(iframe, win, currentTime) {
69263
+ if (applySoftReloadFinalization(iframe, currentTime)) return true;
69264
+ try {
69265
+ win.__player?.seek?.(currentTime);
69266
+ win.__hfStudioManualEditsApply?.();
69267
+ return true;
69268
+ } catch {
69269
+ return false;
69270
+ }
69271
+ }
69272
+ function showRestoreInPlace(iframe, activeCompPath, files, currentTime) {
69273
+ const doc = iframe?.contentDocument;
69274
+ const win = iframe?.contentWindow;
69275
+ const plan = doc && win ? planRestoreTargets(doc, activeCompPath ?? "index.html", files) : null;
69276
+ if (!iframe || !win || !plan || plan.scripted) return null;
69277
+ const before = plan.targets.map(({ live }) => [live, live.cloneNode(false)]);
69278
+ for (const { live, restored } of plan.targets) syncElementAttributes(live, restored);
69279
+ const putBack = (time) => {
69280
+ if (iframe.contentDocument !== doc) return false;
69281
+ for (const [live, attributes] of before) syncElementAttributes(live, attributes);
69282
+ return finalizeInPlace(iframe, win, time);
69283
+ };
69284
+ if (finalizeInPlace(iframe, win, currentTime)) return putBack;
69285
+ putBack(currentTime);
69286
+ return null;
69287
+ }
69102
69288
  function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, reload) {
69103
69289
  const activeDocPath = activeCompPath ?? "index.html";
69104
69290
  const paths = files ? Object.keys(files) : [];
@@ -69106,51 +69292,23 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
69106
69292
  markScenesStale(iframe, paths);
69107
69293
  reload();
69108
69294
  };
69109
- if (!iframe || !files || paths.length !== 1 || paths[0] !== activeDocPath) {
69110
- reloadPreview();
69111
- return "full";
69112
- }
69113
- const doc = iframe.contentDocument;
69114
- const win = iframe.contentWindow;
69115
- if (!doc || !win) {
69116
- reloadPreview();
69117
- return "full";
69118
- }
69119
- const { previous, restored } = files[activeDocPath];
69120
- const diff = diffSoftReloadableRestore(previous, restored);
69121
- if (!diff) {
69122
- reloadPreview();
69123
- return "full";
69124
- }
69125
- if (hasAmbiguousGsapScriptChange(previous, restored)) {
69126
- reloadPreview();
69127
- return "full";
69128
- }
69129
- const liveByKey = identityElementMap(doc);
69130
- const restoredByKey = identityElementMap(new DOMParser().parseFromString(restored, "text/html"));
69131
- if (!liveByKey || !restoredByKey) {
69295
+ const doc = iframe?.contentDocument;
69296
+ const win = iframe?.contentWindow;
69297
+ const plan = doc && files && win ? planRestoreTargets(doc, activeDocPath, files) : null;
69298
+ if (!iframe || !doc || !files || !win || !plan) {
69132
69299
  reloadPreview();
69133
69300
  return "full";
69134
69301
  }
69135
- const changedTargets = [];
69136
- for (const key2 of diff.changedElementKeys) {
69137
- const liveEl = liveByKey.get(key2);
69138
- const restoredEl = restoredByKey.get(key2);
69139
- if (!liveEl || !restoredEl) {
69140
- reloadPreview();
69141
- return "full";
69142
- }
69143
- changedTargets.push({ live: liveEl, restored: restoredEl });
69144
- }
69145
- for (const target of changedTargets) syncElementAttributes(target.live, target.restored);
69146
- const restoredScript = extractGsapScriptText(restored);
69147
- const previousScript = extractGsapScriptText(previous);
69302
+ for (const target of plan.targets) syncElementAttributes(target.live, target.restored);
69303
+ const active = files[activeDocPath];
69304
+ const restoredScript = active ? extractGsapScriptText(active.restored) : null;
69305
+ const previousScript = active ? extractGsapScriptText(active.previous) : null;
69148
69306
  if (restoredScript && restoredScript !== previousScript) {
69149
- syncStaleEditMarks(doc, restored);
69307
+ syncStaleEditMarks(doc, active.restored);
69150
69308
  const result = applySoftReload(iframe, restoredScript, {
69151
69309
  onAsyncFailure: reloadPreview,
69152
69310
  currentTimeOverride: currentTime,
69153
- authoredHtml: restored
69311
+ authoredHtml: active.restored
69154
69312
  });
69155
69313
  if (result === "cannot-soft-reload") {
69156
69314
  reloadPreview();
@@ -69158,15 +69316,9 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
69158
69316
  }
69159
69317
  return "soft";
69160
69318
  }
69161
- if (applySoftReloadFinalization(iframe, currentTime)) return "soft";
69162
- try {
69163
- win.__player?.seek?.(currentTime);
69164
- win.__hfStudioManualEditsApply?.();
69165
- } catch {
69166
- reloadPreview();
69167
- return "full";
69168
- }
69169
- return "soft";
69319
+ if (finalizeInPlace(iframe, win, currentTime)) return "soft";
69320
+ reloadPreview();
69321
+ return "full";
69170
69322
  }
69171
69323
 
69172
69324
  // src/player/lib/automationStoreSync.ts
@@ -69350,6 +69502,20 @@ function usePreviewPersistence({
69350
69502
  },
69351
69503
  [previewIframeRef, activeCompPathRef, reloadPreview]
69352
69504
  );
69505
+ const showHistoryRestoreNow = useCallback103(
69506
+ (files) => {
69507
+ if (!domEditSaveQueueRef.current?.isIdle() || hasStudioPendingEdits()) return null;
69508
+ const iframe = previewIframeRef.current;
69509
+ const now2 = () => usePlayerStore.getState().currentTime;
69510
+ const putBack = showRestoreInPlace(iframe, activeCompPathRef.current, files, now2());
69511
+ if (!putBack) return null;
69512
+ return () => {
69513
+ if (putBack(now2())) syncStoredAutomationFromPreview(iframe?.contentDocument ?? null);
69514
+ else void syncHistoryPreviewAfterApply({ paths: Object.keys(files) });
69515
+ };
69516
+ },
69517
+ [previewIframeRef, activeCompPathRef, syncHistoryPreviewAfterApply]
69518
+ );
69353
69519
  useMountEffect(() => {
69354
69520
  void clearLegacyStudioMotionFile(_readOptionalProjectFile, _writeProjectFile);
69355
69521
  });
@@ -69364,7 +69530,8 @@ function usePreviewPersistence({
69364
69530
  resetDomEditSaveQueueBreaker,
69365
69531
  applyCurrentStudioManualEditsToPreview,
69366
69532
  applyStudioManualEditsToPreview,
69367
- syncHistoryPreviewAfterApply
69533
+ syncHistoryPreviewAfterApply,
69534
+ showHistoryRestoreNow
69368
69535
  };
69369
69536
  }
69370
69537
 
@@ -79630,14 +79797,18 @@ function useStudioExternalFileChanges({
79630
79797
  fileManager,
79631
79798
  previewPersistence,
79632
79799
  pendingTimelineEditPathRef,
79633
- reloadPreview
79800
+ reloadPreview,
79801
+ onOutsideChange
79634
79802
  }) {
79635
79803
  const { flushPendingSourceSave, discardPendingSourceSave } = fileManager;
79636
79804
  const { drainPendingDomEditSaves, resetDomEditSaveQueueBreaker } = previewPersistence;
79637
79805
  const bumpThumbnailRevisions = usePlayerStore((state) => state.bumpThumbnailRevisions);
79638
79806
  const onAcceptedPersistedFileChange = useCallback143(
79639
- (_path, affectedCompositions) => bumpThumbnailRevisions(affectedCompositions),
79640
- [bumpThumbnailRevisions]
79807
+ (_path, affectedCompositions) => {
79808
+ onOutsideChange?.();
79809
+ bumpThumbnailRevisions(affectedCompositions);
79810
+ },
79811
+ [bumpThumbnailRevisions, onOutsideChange]
79641
79812
  );
79642
79813
  const drainPendingChanges = useCallback143(async () => {
79643
79814
  const source = await flushPendingSourceSave();
@@ -80204,6 +80375,7 @@ function useEditHistoryActions({
80204
80375
  writeProjectFile,
80205
80376
  showToast,
80206
80377
  syncHistoryPreviewAfterApply,
80378
+ showHistoryRestoreNow,
80207
80379
  waitForPendingDomEditSaves,
80208
80380
  onAfterUndoRedo,
80209
80381
  activeCompPath,
@@ -80220,11 +80392,20 @@ function useEditHistoryActions({
80220
80392
  const apply = useCallback145(
80221
80393
  async (direction) => {
80222
80394
  const noun = direction === "undo" ? "Undo" : "Redo";
80223
- await waitForPendingDomEditSaves();
80224
- const result = await editHistory[direction]({
80225
- readFile: readHistoryFile,
80226
- serialize: serializeHistoryFiles
80227
- });
80395
+ const predicted = editHistory.predict?.(direction) ?? null;
80396
+ const putBack = predicted ? showHistoryRestoreNow?.(predicted.files) ?? null : null;
80397
+ let result = { ok: false, reason: "failed" };
80398
+ let serverSteppedShown = false;
80399
+ try {
80400
+ await waitForPendingDomEditSaves();
80401
+ result = await editHistory[direction]({
80402
+ readFile: readHistoryFile,
80403
+ serialize: serializeHistoryFiles
80404
+ });
80405
+ serverSteppedShown = Boolean(putBack && result.ok && result.label) && result.undoes === predicted?.id;
80406
+ } finally {
80407
+ if (putBack && !serverSteppedShown) putBack();
80408
+ }
80228
80409
  if (!result.ok && result.reason === "content-mismatch") {
80229
80410
  showToast(
80230
80411
  `Can't ${direction}: ${result.paths?.join(", ")} changed since that edit.`,
@@ -80237,7 +80418,8 @@ function useEditHistoryActions({
80237
80418
  return;
80238
80419
  }
80239
80420
  if (result.ok && result.label) {
80240
- const restore = { paths: result.paths, files: result.files };
80421
+ const files = serverSteppedShown ? fromShown(result.files, predicted.files) : result.files;
80422
+ const restore = { paths: result.paths, files };
80241
80423
  onAfterUndoRedo?.(restore);
80242
80424
  if (activeCompPath && result.paths?.includes(activeCompPath)) {
80243
80425
  forceReloadSdkSession?.();
@@ -80251,6 +80433,7 @@ function useEditHistoryActions({
80251
80433
  readHistoryFile,
80252
80434
  showToast,
80253
80435
  syncHistoryPreviewAfterApply,
80436
+ showHistoryRestoreNow,
80254
80437
  waitForPendingDomEditSaves,
80255
80438
  serializeHistoryFiles,
80256
80439
  onAfterUndoRedo,
@@ -80262,6 +80445,15 @@ function useEditHistoryActions({
80262
80445
  const redo = useCallback145(() => apply("redo"), [apply]);
80263
80446
  return useMemo71(() => ({ undo, redo }), [undo, redo]);
80264
80447
  }
80448
+ function fromShown(files, shown2) {
80449
+ if (!files) return files;
80450
+ return Object.fromEntries(
80451
+ Object.entries(files).map(([path, f]) => [
80452
+ path,
80453
+ { previous: shown2[path]?.restored ?? f.previous, restored: f.restored }
80454
+ ])
80455
+ );
80456
+ }
80265
80457
 
80266
80458
  // src/hooks/useAppHotkeys.ts
80267
80459
  function iframeContentWindow(iframe) {
@@ -80277,9 +80469,9 @@ function safeAddListener(t, type, h, capture = false) {
80277
80469
  } catch {
80278
80470
  }
80279
80471
  }
80280
- function safeRemoveListener(t, type, h) {
80472
+ function safeRemoveListener(t, type, h, capture = false) {
80281
80473
  try {
80282
- t?.removeEventListener(type, h);
80474
+ t?.removeEventListener(type, h, capture);
80283
80475
  } catch {
80284
80476
  }
80285
80477
  }
@@ -80306,6 +80498,7 @@ function useAppHotkeys({
80306
80498
  writeProjectFile,
80307
80499
  showToast,
80308
80500
  syncHistoryPreviewAfterApply,
80501
+ showHistoryRestoreNow,
80309
80502
  waitForPendingDomEditSaves,
80310
80503
  handleCopy: handleCopy2,
80311
80504
  handlePaste: handlePaste2,
@@ -80329,6 +80522,7 @@ function useAppHotkeys({
80329
80522
  writeProjectFile,
80330
80523
  showToast,
80331
80524
  syncHistoryPreviewAfterApply,
80525
+ showHistoryRestoreNow,
80332
80526
  waitForPendingDomEditSaves,
80333
80527
  onAfterUndoRedo,
80334
80528
  activeCompPath,
@@ -80385,38 +80579,17 @@ function useAppHotkeys({
80385
80579
  window.addEventListener("keydown", handleAppKeyDown, true);
80386
80580
  return () => window.removeEventListener("keydown", handleAppKeyDown, true);
80387
80581
  }, [handleAppKeyDown]);
80388
- const handleHistoryHotkey = useCallback146((event) => {
80389
- if (!(event.metaKey || event.ctrlKey) || shouldIgnoreHistoryShortcut(event.target)) return;
80390
- handleUndoRedoKey(
80391
- event,
80392
- () => void cbRef.current.handleUndo(),
80393
- () => void cbRef.current.handleRedo()
80394
- );
80395
- }, []);
80396
80582
  const syncPreviewHotkeys = useCallback146(
80397
80583
  (iframe) => {
80398
80584
  previewHistoryCleanupRef.current?.();
80399
80585
  previewHistoryCleanupRef.current = null;
80400
80586
  const win = iframeContentWindow(iframe);
80401
- let doc = null;
80402
- try {
80403
- doc = iframe?.contentDocument ?? null;
80404
- } catch {
80405
- doc = null;
80406
- }
80407
- if (!win && !doc) return;
80408
- const handler = handleHistoryHotkey;
80587
+ if (!win) return;
80409
80588
  const appHandler = handleAppKeyDown;
80410
- safeAddListener(win, "keydown", handler, true);
80411
80589
  safeAddListener(win, "keydown", appHandler, true);
80412
- doc?.addEventListener("keydown", handleHistoryHotkey, true);
80413
- previewHistoryCleanupRef.current = () => {
80414
- safeRemoveListener(win, "keydown", handler);
80415
- safeRemoveListener(win, "keydown", appHandler);
80416
- doc?.removeEventListener("keydown", handleHistoryHotkey, true);
80417
- };
80590
+ previewHistoryCleanupRef.current = () => safeRemoveListener(win, "keydown", appHandler, true);
80418
80591
  },
80419
- [handleAppKeyDown, handleHistoryHotkey]
80592
+ [handleAppKeyDown]
80420
80593
  );
80421
80594
  useEffect121(
80422
80595
  () => () => {
@@ -91802,7 +91975,8 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
91802
91975
  fileManager,
91803
91976
  previewPersistence,
91804
91977
  pendingTimelineEditPathRef,
91805
- reloadPreview
91978
+ reloadPreview,
91979
+ onOutsideChange: editHistory.noteOutsideChange
91806
91980
  });
91807
91981
  const invalidateGsapCacheRef = useRef181(() => {
91808
91982
  });
@@ -91904,6 +92078,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
91904
92078
  writeProjectFile: fileManager.writeProjectFile,
91905
92079
  showToast,
91906
92080
  syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
92081
+ showHistoryRestoreNow: previewPersistence.showHistoryRestoreNow,
91907
92082
  waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
91908
92083
  handleCopy: handleCopy2,
91909
92084
  handlePaste: handlePaste2,
@@ -92792,6 +92967,7 @@ export {
92792
92967
  ConnectedDomEditOverlay,
92793
92968
  ContextMenu,
92794
92969
  DEFAULT_SHORTCUT_SECTIONS,
92970
+ Dock,
92795
92971
  DomEditOverlay,
92796
92972
  DomEditProvider,
92797
92973
  EditorShell,
@@ -92887,6 +93063,7 @@ export {
92887
93063
  snapEngagedForTravel,
92888
93064
  useClipboard,
92889
93065
  useCompositionStack,
93066
+ useDockLayoutStore,
92890
93067
  useDomEditSelectionContext,
92891
93068
  useDomEditSession,
92892
93069
  useDomGeometryCommit,