@hyperframes/studio 0.7.59 → 0.7.61

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 (78) hide show
  1. package/dist/assets/hyperframes-player-Xvx2hkrc.js +459 -0
  2. package/dist/assets/{index-CrkAdJkb.js → index-2mxh_HSy.js} +201 -201
  3. package/dist/assets/{index-B995FG46.js → index-BCpoiv9S.js} +1 -1
  4. package/dist/assets/index-BhWig0mx.css +1 -0
  5. package/dist/assets/{index-FvzmPhfG.js → index-D-uFclFj.js} +1 -1
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +1441 -770
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +4 -9
  11. package/src/components/DesignPanelPromoteProvider.tsx +27 -1
  12. package/src/components/StudioHeader.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +47 -28
  14. package/src/components/editor/PromotableControl.tsx +4 -2
  15. package/src/components/editor/PropertyPanel.tsx +151 -149
  16. package/src/components/editor/PropertyPanelFlat.tsx +44 -42
  17. package/src/components/editor/manualEditingAvailability.test.ts +6 -6
  18. package/src/components/editor/manualEditingAvailability.ts +5 -3
  19. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +2 -2
  20. package/src/components/editor/propertyPanelFlatMotionSection.tsx +1 -1
  21. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +10 -0
  22. package/src/components/editor/propertyPanelFlatPrimitives.tsx +3 -2
  23. package/src/components/editor/propertyPanelFlatSelectRow.tsx +7 -1
  24. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
  25. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +21 -9
  26. package/src/components/editor/propertyPanelFlatStyleSections.tsx +15 -32
  27. package/src/components/editor/propertyPanelFlatTextSection.tsx +3 -3
  28. package/src/components/editor/propertyPanelInputCoverage.test.tsx +14 -0
  29. package/src/components/editor/propertyPanelPrimitives.tsx +9 -1
  30. package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
  31. package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
  32. package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
  33. package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
  34. package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
  35. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
  36. package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
  37. package/src/components/storyboard/storyboardReviewStage.ts +45 -0
  38. package/src/components/storyboard/useFrameComments.ts +22 -6
  39. package/src/contexts/FileManagerContext.tsx +3 -0
  40. package/src/contexts/PanelLayoutContext.tsx +3 -0
  41. package/src/contexts/ViewModeContext.tsx +60 -7
  42. package/src/hooks/timelineEditingHelpers.ts +2 -2
  43. package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
  44. package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
  45. package/src/hooks/useDomEditCommits.test.tsx +41 -1
  46. package/src/hooks/useDomEditCommits.ts +2 -2
  47. package/src/hooks/useDomEditSession.ts +1 -1
  48. package/src/hooks/useEditorSave.ts +1 -1
  49. package/src/hooks/useFileManager.projectOwnership.test.tsx +14 -3
  50. package/src/hooks/useFileManager.ts +76 -12
  51. package/src/hooks/usePanelLayout.test.ts +118 -0
  52. package/src/hooks/usePanelLayout.ts +24 -1
  53. package/src/hooks/usePreviewPersistence.ts +4 -1
  54. package/src/hooks/useRazorSplit.history.test.tsx +12 -4
  55. package/src/hooks/useRazorSplit.test.tsx +88 -0
  56. package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
  57. package/src/hooks/useRazorSplit.ts +50 -16
  58. package/src/hooks/useSlideshowTabState.test.ts +96 -0
  59. package/src/hooks/useSlideshowTabState.ts +61 -0
  60. package/src/hooks/useTimelineEditing.ts +2 -0
  61. package/src/hooks/useTimelineEditingTypes.ts +2 -1
  62. package/src/hooks/useTimelineGroupEditing.ts +1 -1
  63. package/src/player/lib/playbackTypes.ts +7 -0
  64. package/src/player/lib/runtimeProtocol.test.ts +6 -1
  65. package/src/player/lib/timelineDOM.ts +8 -11
  66. package/src/player/lib/timelineIframeHelpers.ts +153 -107
  67. package/src/utils/domEditSaveQueue.test.ts +19 -0
  68. package/src/utils/domEditSaveQueue.ts +2 -1
  69. package/src/utils/sdkCutover.test.ts +21 -5
  70. package/src/utils/sdkEditTransaction.ts +5 -4
  71. package/src/utils/studioFileHistory.ts +6 -4
  72. package/src/utils/studioFileVersion.test.ts +36 -0
  73. package/src/utils/studioFileVersion.ts +23 -0
  74. package/src/utils/studioSaveDiagnostics.test.ts +18 -0
  75. package/src/utils/studioSaveDiagnostics.ts +21 -0
  76. package/dist/assets/hyperframes-player-Bm07FLkl.js +0 -459
  77. package/dist/assets/index-Dj5p8U_A.css +0 -1
  78. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
package/dist/index.js CHANGED
@@ -686,6 +686,9 @@ function formatFrameTime(time, duration, fps = STUDIO_PREVIEW_FPS) {
686
686
  return `${currentFrame}f / ${totalFrames}f`;
687
687
  }
688
688
 
689
+ // src/player/lib/timelineIframeHelpers.ts
690
+ import { readClipTiming } from "@hyperframes/core/composition-contract";
691
+
689
692
  // src/player/lib/playbackAdapter.ts
690
693
  function isFinitePositive(value) {
691
694
  return Number.isFinite(value) && value > 0;
@@ -1239,95 +1242,130 @@ function stopScrubPreviewAudio() {
1239
1242
  scrubPrevMuted = null;
1240
1243
  scrubPrevVolume = null;
1241
1244
  }
1245
+ function timelineDuration(iframeWin, compositionId) {
1246
+ return iframeWin.__timelines?.[compositionId]?.duration?.() ?? 0;
1247
+ }
1248
+ function createTimedElementLookup(doc) {
1249
+ const timedById = /* @__PURE__ */ new Map();
1250
+ for (const timed of doc.querySelectorAll("[data-start]")) {
1251
+ for (const id of [
1252
+ timed.id,
1253
+ timed.getAttribute("data-hf-id"),
1254
+ timed.getAttribute("data-composition-id")
1255
+ ]) {
1256
+ if (id) timedById.set(id, timed);
1257
+ }
1258
+ }
1259
+ return timedById;
1260
+ }
1261
+ function createReferenceEndResolver(timedById, iframeWin) {
1262
+ const resolveEnd = (refId, visiting) => {
1263
+ if (visiting.has(refId)) return null;
1264
+ const referenced = timedById.get(refId);
1265
+ if (!referenced) return null;
1266
+ const next = new Set(visiting).add(refId);
1267
+ const timing = readClipTiming(referenced, {
1268
+ resolveReferenceEnd: (nestedId) => resolveEnd(nestedId, next)
1269
+ });
1270
+ if (timing.end != null) return timing.end;
1271
+ const compositionId = referenced.getAttribute("data-composition-id");
1272
+ const duration = compositionId ? timelineDuration(iframeWin, compositionId) : 0;
1273
+ return timing.start == null || duration <= 0 ? null : timing.start + duration;
1274
+ };
1275
+ return resolveEnd;
1276
+ }
1277
+ function clampCompositionWindow(start, duration, rootDuration) {
1278
+ if (!Number.isFinite(duration) || duration <= 0) return null;
1279
+ const safeStart = Number.isFinite(start) ? start : 0;
1280
+ if (!Number.isFinite(rootDuration) || rootDuration <= 0) {
1281
+ return { start: safeStart, duration };
1282
+ }
1283
+ if (safeStart >= rootDuration) return null;
1284
+ const clamped = Math.min(duration, Math.max(0, rootDuration - safeStart));
1285
+ return clamped > 0 ? { start: safeStart, duration: clamped } : null;
1286
+ }
1287
+ function nonEmpty(value, fallback) {
1288
+ return value || fallback;
1289
+ }
1290
+ function optionalNonEmpty(value) {
1291
+ return value || void 0;
1292
+ }
1293
+ function attachCompositionSource(entry, element, compositionSrc) {
1294
+ if (compositionSrc) return { ...entry, compositionSrc };
1295
+ const innerVideo = element.querySelector("video[src]");
1296
+ if (!innerVideo) return entry;
1297
+ return { ...entry, src: optionalNonEmpty(innerVideo.getAttribute("src")), tag: "video" };
1298
+ }
1299
+ function buildMissingCompositionEntry(params) {
1300
+ const { doc, iframeWin, element, compositionId, rootDuration, fallbackIndex, resolveEnd } = params;
1301
+ const timing = readClipTiming(element, {
1302
+ resolveReferenceEnd: (refId) => resolveEnd(refId, /* @__PURE__ */ new Set([compositionId]))
1303
+ });
1304
+ const window2 = clampCompositionWindow(
1305
+ timing.start ?? 0,
1306
+ timing.duration ?? timelineDuration(iframeWin, compositionId),
1307
+ rootDuration
1308
+ );
1309
+ if (!window2) return null;
1310
+ const preferredId = nonEmpty(element.id, compositionId);
1311
+ const compositionSrc = element.getAttribute("data-composition-src") ?? element.getAttribute("data-composition-file");
1312
+ const selector = getTimelineElementSelector(element);
1313
+ const sourceFile = getTimelineElementSourceFile(element);
1314
+ const selectorIndex = getTimelineElementSelectorIndex(doc, element, selector);
1315
+ const label = getTimelineElementDisplayLabel({
1316
+ id: preferredId,
1317
+ label: element.getAttribute("data-timeline-label") ?? element.getAttribute("data-label"),
1318
+ tag: element.tagName
1319
+ });
1320
+ const identity = buildTimelineElementIdentity({
1321
+ preferredId,
1322
+ label,
1323
+ fallbackIndex,
1324
+ domId: optionalNonEmpty(element.id),
1325
+ selector,
1326
+ selectorIndex,
1327
+ sourceFile
1328
+ });
1329
+ const entry = {
1330
+ id: identity.id,
1331
+ label,
1332
+ key: identity.key,
1333
+ tag: element.tagName.toLowerCase(),
1334
+ start: window2.start,
1335
+ duration: window2.duration,
1336
+ track: timing.trackIndex,
1337
+ domId: optionalNonEmpty(element.id),
1338
+ hfId: optionalNonEmpty(element.getAttribute("data-hf-id")),
1339
+ selector,
1340
+ selectorIndex,
1341
+ sourceFile,
1342
+ zIndex: readTimelineElementZIndex(element)
1343
+ };
1344
+ return attachCompositionSource(entry, element, compositionSrc);
1345
+ }
1242
1346
  function buildMissingCompositionElements(doc, iframeWin, currentEls, rootDuration) {
1243
1347
  const existingIds = new Set(currentEls.map((e) => e.id));
1244
1348
  const rootComp = doc.querySelector("[data-composition-id]");
1245
1349
  const rootCompId = rootComp?.getAttribute("data-composition-id");
1246
1350
  const hosts = doc.querySelectorAll("[data-composition-id][data-start]");
1247
1351
  const missing = [];
1248
- hosts.forEach((host) => {
1352
+ const resolveEnd = createReferenceEndResolver(createTimedElementLookup(doc), iframeWin);
1353
+ for (const host of hosts) {
1249
1354
  const el = host;
1250
1355
  const compId = el.getAttribute("data-composition-id");
1251
- if (!compId || compId === rootCompId) return;
1252
- if (existingIds.has(el.id) || existingIds.has(compId)) return;
1253
- const startAttr = el.getAttribute("data-start") ?? "0";
1254
- let start = parseFloat(startAttr);
1255
- if (isNaN(start)) {
1256
- const ref = doc.getElementById(startAttr) || doc.querySelector(`[data-composition-id="${CSS.escape(startAttr)}"]`);
1257
- if (ref) {
1258
- const refStartAttr = ref.getAttribute("data-start") ?? "0";
1259
- let refStart = parseFloat(refStartAttr);
1260
- if (isNaN(refStart)) {
1261
- const refRef = doc.getElementById(refStartAttr) || doc.querySelector(`[data-composition-id="${CSS.escape(refStartAttr)}"]`);
1262
- const rrStart = parseFloat(refRef?.getAttribute("data-start") ?? "0") || 0;
1263
- const rrCompId = refRef?.getAttribute("data-composition-id");
1264
- const rrDur = parseFloat(refRef?.getAttribute("data-duration") ?? "") || (rrCompId ? iframeWin.__timelines?.[rrCompId]?.duration?.() ?? 0 : 0);
1265
- refStart = rrStart + rrDur;
1266
- }
1267
- const refCompId = ref.getAttribute("data-composition-id");
1268
- const refDur = parseFloat(ref.getAttribute("data-duration") ?? "") || (refCompId ? iframeWin.__timelines?.[refCompId]?.duration?.() ?? 0 : 0);
1269
- start = refStart + refDur;
1270
- } else {
1271
- start = 0;
1272
- }
1273
- }
1274
- let dur = parseFloat(el.getAttribute("data-duration") ?? "");
1275
- if (isNaN(dur) || dur <= 0) {
1276
- dur = iframeWin.__timelines?.[compId]?.duration?.() ?? 0;
1277
- }
1278
- if (!Number.isFinite(dur) || dur <= 0) return;
1279
- if (!Number.isFinite(start)) start = 0;
1280
- if (Number.isFinite(rootDuration) && rootDuration > 0) {
1281
- if (start >= rootDuration) return;
1282
- dur = Math.min(dur, Math.max(0, rootDuration - start));
1283
- if (dur <= 0) return;
1284
- }
1285
- const trackStr = el.getAttribute("data-track-index");
1286
- const track = trackStr != null ? parseInt(trackStr, 10) : 0;
1287
- const compSrc = el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
1288
- const selector = getTimelineElementSelector(el);
1289
- const sourceFile = getTimelineElementSourceFile(el);
1290
- const selectorIndex = getTimelineElementSelectorIndex(doc, el, selector);
1291
- const label = getTimelineElementDisplayLabel({
1292
- id: el.id || compId || null,
1293
- label: el.getAttribute("data-timeline-label") ?? el.getAttribute("data-label"),
1294
- tag: el.tagName
1295
- });
1296
- const identity = buildTimelineElementIdentity({
1297
- preferredId: el.id || compId || null,
1298
- label,
1356
+ if (!compId || compId === rootCompId) continue;
1357
+ if (existingIds.has(el.id) || existingIds.has(compId)) continue;
1358
+ const entry = buildMissingCompositionEntry({
1359
+ doc,
1360
+ iframeWin,
1361
+ element: el,
1362
+ compositionId: compId,
1363
+ rootDuration,
1299
1364
  fallbackIndex: missing.length,
1300
- domId: el.id || void 0,
1301
- selector,
1302
- selectorIndex,
1303
- sourceFile
1365
+ resolveEnd
1304
1366
  });
1305
- const entry = {
1306
- id: identity.id,
1307
- label,
1308
- key: identity.key,
1309
- tag: el.tagName.toLowerCase(),
1310
- start,
1311
- duration: dur,
1312
- track: isNaN(track) ? 0 : track,
1313
- domId: el.id || void 0,
1314
- hfId: el.getAttribute("data-hf-id") || void 0,
1315
- selector,
1316
- selectorIndex,
1317
- sourceFile,
1318
- zIndex: readTimelineElementZIndex(el)
1319
- };
1320
- if (compSrc) {
1321
- entry.compositionSrc = compSrc;
1322
- } else {
1323
- const innerVideo = el.querySelector("video[src]");
1324
- if (innerVideo) {
1325
- entry.src = innerVideo.getAttribute("src") || void 0;
1326
- entry.tag = "video";
1327
- }
1328
- }
1329
- missing.push(entry);
1330
- });
1367
+ if (entry) missing.push(entry);
1368
+ }
1331
1369
  let patched = false;
1332
1370
  const updatedEls = currentEls.map((existing) => {
1333
1371
  if (existing.compositionSrc) return existing;
@@ -2915,6 +2953,7 @@ function FileManagerProvider({
2915
2953
  readProjectFile,
2916
2954
  writeProjectFile,
2917
2955
  readOptionalProjectFile,
2956
+ observeProjectFileVersion,
2918
2957
  updateEditingFileContent,
2919
2958
  revealSourceOffset,
2920
2959
  openSourceForSelection,
@@ -2951,6 +2990,7 @@ function FileManagerProvider({
2951
2990
  readProjectFile,
2952
2991
  writeProjectFile,
2953
2992
  readOptionalProjectFile,
2993
+ observeProjectFileVersion,
2954
2994
  updateEditingFileContent,
2955
2995
  revealSourceOffset,
2956
2996
  openSourceForSelection,
@@ -2984,6 +3024,7 @@ function FileManagerProvider({
2984
3024
  readProjectFile,
2985
3025
  writeProjectFile,
2986
3026
  readOptionalProjectFile,
3027
+ observeProjectFileVersion,
2987
3028
  updateEditingFileContent,
2988
3029
  revealSourceOffset,
2989
3030
  openSourceForSelection,
@@ -3308,6 +3349,7 @@ import { useMemo as useMemo3 } from "react";
3308
3349
 
3309
3350
  // src/player/lib/timelineDOM.ts
3310
3351
  import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
3352
+ import { readClipTiming as readClipTiming2 } from "@hyperframes/core/composition-contract";
3311
3353
  function resolveClipTag(clip) {
3312
3354
  return clip.tagName || clip.kind || "div";
3313
3355
  }
@@ -3463,22 +3505,18 @@ function parseTimelineFromDOM(doc, rootDuration) {
3463
3505
  if (node === rootComp) return;
3464
3506
  if (isTimelineIgnoredElement(node)) return;
3465
3507
  const el = node;
3466
- const startStr = el.getAttribute("data-start");
3467
- if (startStr == null) return;
3468
- const start = parseFloat(startStr);
3469
- if (isNaN(start)) return;
3508
+ const timing = readClipTiming2(el);
3509
+ const start = timing.start;
3510
+ if (start == null) return;
3470
3511
  if (Number.isFinite(rootDuration) && rootDuration > 0 && start >= rootDuration) return;
3471
3512
  const tagLower = el.tagName.toLowerCase();
3472
- let dur = 0;
3473
- const durStr = el.getAttribute("data-duration");
3474
- if (durStr != null) dur = parseFloat(durStr);
3475
- if (isNaN(dur) || dur <= 0) dur = Math.max(0, rootDuration - start);
3513
+ let dur = timing.duration ?? 0;
3514
+ if (dur <= 0) dur = Math.max(0, rootDuration - start);
3476
3515
  if (Number.isFinite(rootDuration) && rootDuration > 0) {
3477
3516
  dur = Math.min(dur, Math.max(0, rootDuration - start));
3478
3517
  }
3479
3518
  if (!Number.isFinite(dur) || dur <= 0) return;
3480
- const trackStr = el.getAttribute("data-track-index");
3481
- const track = trackStr != null ? parseInt(trackStr, 10) : trackCounter++;
3519
+ const track = timing.trackSource === "default" ? trackCounter++ : timing.trackIndex;
3482
3520
  const compId = el.getAttribute("data-composition-id");
3483
3521
  const selector = getTimelineElementSelector(el);
3484
3522
  const sourceFile = getTimelineElementSourceFile(el);
@@ -3504,7 +3542,7 @@ function parseTimelineFromDOM(doc, rootDuration) {
3504
3542
  tag: tagLower,
3505
3543
  start,
3506
3544
  duration: dur,
3507
- track: isNaN(track) ? 0 : track,
3545
+ track,
3508
3546
  domId: el.id || void 0,
3509
3547
  hfId: el.getAttribute("data-hf-id") || void 0,
3510
3548
  selector,
@@ -6495,7 +6533,7 @@ var STUDIO_SDK_RESOLVER_SHADOW_ENABLED = resolveStudioBooleanEnvFlag(
6495
6533
  var STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
6496
6534
  env,
6497
6535
  ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
6498
- false
6536
+ true
6499
6537
  );
6500
6538
  var STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled";
6501
6539
 
@@ -6920,6 +6958,20 @@ var StudioSaveNetworkError = class extends Error {
6920
6958
  this.name = "StudioSaveNetworkError";
6921
6959
  }
6922
6960
  };
6961
+ var StudioFileConflictError = class extends StudioSaveHttpError {
6962
+ filePath;
6963
+ currentVersion;
6964
+ currentContent;
6965
+ attemptedContent;
6966
+ constructor(input) {
6967
+ super(`Save conflict: ${input.filePath} changed outside this Studio session`, 409);
6968
+ this.name = "StudioFileConflictError";
6969
+ this.filePath = input.filePath;
6970
+ this.currentVersion = input.currentVersion;
6971
+ this.currentContent = input.currentContent;
6972
+ this.attemptedContent = input.attemptedContent;
6973
+ }
6974
+ };
6923
6975
  function readNumericProperty(value, key) {
6924
6976
  const record = value;
6925
6977
  const property = record[key];
@@ -7093,14 +7145,14 @@ async function saveProjectFilesWithHistory({
7093
7145
  const writtenPaths = [];
7094
7146
  try {
7095
7147
  for (const path of changedPaths) {
7096
- await writeFile(path, snapshots[path].after);
7148
+ await writeFile(path, snapshots[path].after, snapshots[path].before);
7097
7149
  writtenPaths.push(path);
7098
7150
  }
7099
7151
  await recordEdit({ label, kind, coalesceKey, coalesceMs, files: snapshots });
7100
7152
  } catch (error) {
7101
7153
  try {
7102
7154
  for (const path of writtenPaths.reverse()) {
7103
- await writeFile(path, snapshots[path].before);
7155
+ await writeFile(path, snapshots[path].before, snapshots[path].after);
7104
7156
  }
7105
7157
  } catch (rollbackError) {
7106
7158
  throw new AggregateError(
@@ -8748,7 +8800,7 @@ function snapTimelineTime(time, targets, thresholdSecs) {
8748
8800
  }
8749
8801
  return best ? { time: best.time, target: best } : { time, target: null };
8750
8802
  }
8751
- function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDuration) {
8803
+ function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDuration2) {
8752
8804
  if (targets.length === 0) return { start, snapTime: null, snapType: null };
8753
8805
  const thresholdSecs = TIMELINE_SNAP_PX / Math.max(pixelsPerSecond, 1);
8754
8806
  const startSnap = snapTimelineTime(start, targets, thresholdSecs);
@@ -8764,7 +8816,7 @@ function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDu
8764
8816
  candidate = endSnap.time - duration;
8765
8817
  target = endSnap.target;
8766
8818
  }
8767
- const maxStart = Math.max(0, timelineDuration - duration);
8819
+ const maxStart = Math.max(0, timelineDuration2 - duration);
8768
8820
  const roundedCandidate = Math.round(candidate * 1e3) / 1e3;
8769
8821
  const clamped = Math.max(0, Math.min(maxStart, roundedCandidate));
8770
8822
  if (target && Math.abs(clamped - roundedCandidate) > 1e-6) target = null;
@@ -24480,11 +24532,11 @@ async function buildCandidateEdit(live, deps, mutate, sourceSnapshot) {
24480
24532
  function isCutoverResult(value) {
24481
24533
  return "status" in value;
24482
24534
  }
24483
- async function rollbackWrite(targetPath, originalContent, deps, cause) {
24535
+ async function rollbackWrite(targetPath, originalContent, expectedCurrentContent, deps, cause) {
24484
24536
  try {
24485
24537
  deps.domEditSaveTimestampRef.current = Date.now();
24486
24538
  markSelfWrite(targetPath, originalContent);
24487
- await deps.writeProjectFile(targetPath, originalContent);
24539
+ await deps.writeProjectFile(targetPath, originalContent, expectedCurrentContent);
24488
24540
  return cause;
24489
24541
  } catch (rollbackError) {
24490
24542
  return new AggregateError(
@@ -24497,7 +24549,7 @@ async function writeAndRecord(after, targetPath, originalContent, deps, options)
24497
24549
  deps.domEditSaveTimestampRef.current = Date.now();
24498
24550
  markSelfWrite(targetPath, after);
24499
24551
  try {
24500
- await deps.writeProjectFile(targetPath, after);
24552
+ await deps.writeProjectFile(targetPath, after, originalContent);
24501
24553
  } catch (error) {
24502
24554
  return asCutoverError(error);
24503
24555
  }
@@ -24511,7 +24563,7 @@ async function writeAndRecord(after, targetPath, originalContent, deps, options)
24511
24563
  });
24512
24564
  return null;
24513
24565
  } catch (error) {
24514
- return rollbackWrite(targetPath, originalContent, deps, asCutoverError(error));
24566
+ return rollbackWrite(targetPath, originalContent, after, deps, asCutoverError(error));
24515
24567
  }
24516
24568
  }
24517
24569
  function refreshCommittedEdit(after, deps, options) {
@@ -28329,6 +28381,7 @@ function CommitField({
28329
28381
  value,
28330
28382
  disabled,
28331
28383
  liveCommit,
28384
+ align = "left",
28332
28385
  onCommit
28333
28386
  }) {
28334
28387
  const [draft, setDraft] = useState35(value);
@@ -28401,7 +28454,7 @@ function CommitField({
28401
28454
  scheduleCommit(nextDraft);
28402
28455
  },
28403
28456
  title: parseNumericToken(value) ? "Scroll or use Arrow keys to adjust" : void 0,
28404
- className: "min-w-0 w-full bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
28457
+ className: `min-w-0 w-full bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600 ${align === "right" ? "text-right" : "text-left"}`
28405
28458
  }
28406
28459
  );
28407
28460
  }
@@ -31826,7 +31879,7 @@ function PromotableControl({
31826
31879
  bound && /* @__PURE__ */ jsxs58(
31827
31880
  "span",
31828
31881
  {
31829
- className: "pointer-events-none absolute right-1.5 top-0 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent",
31882
+ className: "pointer-events-none absolute -top-2 right-1.5 z-10 inline-flex max-w-[60%] items-center gap-1 truncate rounded bg-studio-accent/20 px-1 py-px font-mono text-[8px] font-medium text-studio-accent",
31830
31883
  title: `Bound to variable "${promote.boundId}"`,
31831
31884
  children: [
31832
31885
  "\u25C6 ",
@@ -31843,7 +31896,7 @@ function PromotableControl({
31843
31896
  e.stopPropagation();
31844
31897
  promote.promote();
31845
31898
  },
31846
- className: "absolute right-1.5 top-0 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100",
31899
+ className: "absolute -top-2 right-1.5 z-10 inline-flex items-center gap-1 rounded bg-neutral-800/80 px-1 py-px font-mono text-[8px] font-medium text-neutral-400 opacity-70 transition-colors hover:bg-studio-accent/20 hover:text-studio-accent hover:opacity-100",
31847
31900
  children: "\u25C7 var"
31848
31901
  }
31849
31902
  )
@@ -36177,33 +36230,39 @@ function FlatSelectRow({
36177
36230
  return /* @__PURE__ */ jsxs75("div", { className: "group flex min-h-[30px] items-center justify-between", children: [
36178
36231
  /* @__PURE__ */ jsx88("span", { className: `text-[11px] ${VALUE_TIER_LABEL_CLASS[tier]}`, children: label }),
36179
36232
  /* @__PURE__ */ jsxs75("span", { className: "flex items-center gap-2", children: [
36180
- /* @__PURE__ */ jsxs75("label", { className: "flex items-center gap-1.5", children: [
36181
- /* @__PURE__ */ jsx88(
36182
- "select",
36183
- {
36184
- value,
36185
- disabled,
36186
- "aria-label": ariaLabel || label || void 0,
36187
- onChange: (e) => {
36188
- track("select", trackName);
36189
- onChange(e.target.value);
36190
- },
36191
- className: `appearance-none bg-transparent text-right font-mono text-[11px] outline-none disabled:cursor-not-allowed ${VALUE_TIER_VALUE_CLASS[tier]}`,
36192
- children: renderedOptions.map((option) => /* @__PURE__ */ jsx88("option", { value: option.value, children: option.label }, option.value))
36193
- }
36194
- ),
36195
- /* @__PURE__ */ jsx88(
36196
- "svg",
36197
- {
36198
- width: "10",
36199
- height: "10",
36200
- viewBox: "0 0 10 10",
36201
- fill: "currentColor",
36202
- className: "flex-shrink-0 text-panel-text-5",
36203
- children: /* @__PURE__ */ jsx88("path", { d: "M2 3l3 4 3-4z" })
36204
- }
36205
- )
36206
- ] }),
36233
+ /* @__PURE__ */ jsxs75(
36234
+ "label",
36235
+ {
36236
+ className: `flex items-center gap-1.5 border-b pb-px ${tier === "explicitCustom" ? "border-panel-accent/30 group-hover:border-panel-accent/70" : "border-panel-border-input/50 group-hover:border-panel-border-input"}`,
36237
+ children: [
36238
+ /* @__PURE__ */ jsx88(
36239
+ "select",
36240
+ {
36241
+ value,
36242
+ disabled,
36243
+ "aria-label": ariaLabel || label || void 0,
36244
+ onChange: (e) => {
36245
+ track("select", trackName);
36246
+ onChange(e.target.value);
36247
+ },
36248
+ className: `appearance-none bg-transparent text-right font-mono text-[11px] outline-none disabled:cursor-not-allowed ${VALUE_TIER_VALUE_CLASS[tier]}`,
36249
+ children: renderedOptions.map((option) => /* @__PURE__ */ jsx88("option", { value: option.value, children: option.label }, option.value))
36250
+ }
36251
+ ),
36252
+ /* @__PURE__ */ jsx88(
36253
+ "svg",
36254
+ {
36255
+ width: "10",
36256
+ height: "10",
36257
+ viewBox: "0 0 10 10",
36258
+ fill: "currentColor",
36259
+ className: "flex-shrink-0 text-panel-text-5",
36260
+ children: /* @__PURE__ */ jsx88("path", { d: "M2 3l3 4 3-4z" })
36261
+ }
36262
+ )
36263
+ ]
36264
+ }
36265
+ ),
36207
36266
  tier === "explicitCustom" && onReset && /* @__PURE__ */ jsx88(
36208
36267
  "button",
36209
36268
  {
@@ -36244,13 +36303,14 @@ function FlatRow({
36244
36303
  "span",
36245
36304
  {
36246
36305
  "data-flat-row-value": "true",
36247
- className: `min-w-0 border-b pb-px font-mono text-[11px] ${VALUE_TIER_VALUE_CLASS[tier]} ${tier === "explicitCustom" ? "border-transparent group-hover:border-panel-accent/35" : "border-transparent group-hover:border-panel-border-input"}`,
36306
+ className: `min-w-0 border-b pb-px font-mono text-[11px] ${VALUE_TIER_VALUE_CLASS[tier]} ${tier === "explicitCustom" ? "border-panel-accent/30 group-hover:border-panel-accent/70" : "border-panel-border-input/50 group-hover:border-panel-border-input"}`,
36248
36307
  children: /* @__PURE__ */ jsx89(
36249
36308
  CommitField,
36250
36309
  {
36251
36310
  value,
36252
36311
  disabled,
36253
36312
  liveCommit,
36313
+ align: "right",
36254
36314
  onCommit: (nextValue) => {
36255
36315
  track("metric", label);
36256
36316
  onCommit(nextValue);
@@ -36680,7 +36740,7 @@ function FlatTextFieldEditor({
36680
36740
  children: "Weight"
36681
36741
  }
36682
36742
  ),
36683
- /* @__PURE__ */ jsxs77("label", { className: "flex items-center gap-1.5", children: [
36743
+ /* @__PURE__ */ jsxs77("label", { className: "flex items-center gap-1.5 border-b border-panel-border-input/50 pb-px hover:border-panel-border-input", children: [
36684
36744
  /* @__PURE__ */ jsx90(
36685
36745
  "select",
36686
36746
  {
@@ -36822,7 +36882,7 @@ function FlatTextSection({
36822
36882
  const activeField = textFields.find((field) => field.key === activeFieldKey) ?? textFields[0];
36823
36883
  if (!activeField) return null;
36824
36884
  if (textFields.length > 1) {
36825
- return /* @__PURE__ */ jsxs77("div", { className: "space-y-1.5", children: [
36885
+ return /* @__PURE__ */ jsxs77("div", { className: "space-y-2.5", children: [
36826
36886
  /* @__PURE__ */ jsx90(
36827
36887
  FlatTextLayerList,
36828
36888
  {
@@ -36851,7 +36911,7 @@ function FlatTextSection({
36851
36911
  )
36852
36912
  ] });
36853
36913
  }
36854
- return /* @__PURE__ */ jsxs77("div", { className: "space-y-1.5", children: [
36914
+ return /* @__PURE__ */ jsxs77("div", { className: "space-y-2.5", children: [
36855
36915
  /* @__PURE__ */ jsx90(
36856
36916
  FlatTextFieldEditor,
36857
36917
  {
@@ -36965,17 +37025,6 @@ var STROKE_STYLE_OPTIONS = [
36965
37025
  "inset",
36966
37026
  "outset"
36967
37027
  ];
36968
- function formatStrokeSummary(widthPx, style) {
36969
- return `${widthPx}px ${style}`;
36970
- }
36971
- function parseStrokeSummary(text) {
36972
- const match = /^\s*(-?\d+(?:\.\d+)?)px\s+(\S+)\s*$/.exec(text);
36973
- if (!match) return null;
36974
- const widthPx = Number.parseFloat(match[1]);
36975
- const style = match[2];
36976
- if (!Number.isFinite(widthPx) || !style) return null;
36977
- return { widthPx, style };
36978
- }
36979
37028
 
36980
37029
  // src/components/editor/propertyPanelFlatMaskInsetRows.tsx
36981
37030
  import { Fragment as Fragment28, jsx as jsx91, jsxs as jsxs78 } from "react/jsx-runtime";
@@ -37164,24 +37213,17 @@ function FlatStrokeRow({
37164
37213
  const borderWidthValue = parsePxMetricValue(styles["border-width"] ?? "") ?? parsePxMetricValue(styles["border-top-width"] ?? "") ?? 0;
37165
37214
  const borderStyleValue = styles["border-style"] || styles["border-top-style"] || "none";
37166
37215
  const borderColorValue = styles["border-color"] || styles["border-top-color"] || "rgba(255, 255, 255, 0.18)";
37167
- const summary = formatStrokeSummary(borderWidthValue, borderStyleValue);
37168
- const tier = resolveValueTier(
37169
- styles["border-width"] != null || styles["border-style"] != null ? summary : void 0,
37170
- formatStrokeSummary(0, "none")
37171
- );
37216
+ const widthDisplay = formatPxMetricValue(borderWidthValue);
37172
37217
  return /* @__PURE__ */ jsxs79(Fragment29, { children: [
37173
37218
  /* @__PURE__ */ jsx92(
37174
37219
  FlatRow,
37175
37220
  {
37176
- label: "Stroke",
37177
- value: summary,
37178
- tier,
37221
+ label: "Stroke width",
37222
+ value: widthDisplay,
37223
+ tier: resolveValueTier(styles["border-width"], "0px"),
37179
37224
  disabled,
37180
37225
  onCommit: async (next) => {
37181
- const parsed = parseStrokeSummary(next);
37182
- if (!parsed) return;
37183
- if (!STROKE_STYLE_OPTIONS.includes(parsed.style)) return;
37184
- const normalizedWidth = normalizePanelPxValue(`${parsed.widthPx}px`, {
37226
+ const normalizedWidth = normalizePanelPxValue(next, {
37185
37227
  min: 0,
37186
37228
  max: 200,
37187
37229
  fallback: borderWidthValue
@@ -37189,24 +37231,11 @@ function FlatStrokeRow({
37189
37231
  if (!normalizedWidth) return;
37190
37232
  for (const [property, value] of buildStrokeWidthStyleUpdates(
37191
37233
  normalizedWidth,
37192
- parsed.style
37234
+ borderStyleValue
37193
37235
  )) {
37194
37236
  await onSetStyle(property, value);
37195
37237
  }
37196
- for (const [property, value] of buildStrokeStyleUpdates(parsed.style, normalizedWidth)) {
37197
- await onSetStyle(property, value);
37198
- }
37199
- },
37200
- suffix: /* @__PURE__ */ jsxs79(Fragment29, { children: [
37201
- /* @__PURE__ */ jsx92(
37202
- "span",
37203
- {
37204
- className: "h-4 w-4 flex-shrink-0 rounded border border-panel-border-input",
37205
- style: { backgroundColor: borderColorValue }
37206
- }
37207
- ),
37208
- /* @__PURE__ */ jsx92("span", { className: "font-mono text-[10px] text-panel-text-3", children: borderColorValue })
37209
- ] })
37238
+ }
37210
37239
  }
37211
37240
  ),
37212
37241
  /* @__PURE__ */ jsx92(
@@ -37912,7 +37941,7 @@ function FlatTimingRow({
37912
37941
  };
37913
37942
  const cell = (label, value, onCommit) => /* @__PURE__ */ jsxs82("div", { className: "grid gap-px", children: [
37914
37943
  /* @__PURE__ */ jsx95("span", { className: "text-[9px] text-panel-text-4", children: label }),
37915
- /* @__PURE__ */ jsx95("span", { className: "border-b border-transparent font-mono text-[11px] text-panel-text-0 hover:border-panel-border-input", children: /* @__PURE__ */ jsx95(
37944
+ /* @__PURE__ */ jsx95("span", { className: "border-b border-panel-border-input/50 font-mono text-[11px] text-panel-text-0 hover:border-panel-border-input", children: /* @__PURE__ */ jsx95(
37916
37945
  CommitField,
37917
37946
  {
37918
37947
  value,
@@ -38724,7 +38753,7 @@ function FlatColorGradingSection({
38724
38753
  track("select", "Custom LUT");
38725
38754
  applyLut(src || null, src && lut?.src === src ? lut.intensity : 1);
38726
38755
  },
38727
- className: "bg-transparent font-mono text-[10px] text-panel-text-3 outline-none",
38756
+ className: "border-b border-panel-border-input/50 bg-transparent font-mono text-[10px] text-panel-text-3 outline-none hover:border-panel-border-input",
38728
38757
  children: [
38729
38758
  /* @__PURE__ */ jsx98("option", { value: "", children: "None" }),
38730
38759
  lutAssets.map((asset) => /* @__PURE__ */ jsx98("option", { value: asset, children: asset.split("/").pop() ?? asset }, asset))
@@ -38876,7 +38905,7 @@ function FlatColorGradingSection({
38876
38905
  onSetApplyScope(e.target.value);
38877
38906
  },
38878
38907
  disabled: applyBusy,
38879
- className: "bg-transparent font-mono text-[11px] text-panel-text-0 outline-none disabled:opacity-50",
38908
+ className: "border-b border-panel-border-input/50 bg-transparent font-mono text-[11px] text-panel-text-0 outline-none hover:border-panel-border-input disabled:opacity-50",
38880
38909
  children: [
38881
38910
  /* @__PURE__ */ jsx98("option", { value: "source-file", children: "Current file media" }),
38882
38911
  /* @__PURE__ */ jsx98("option", { value: "project", children: "All project media" })
@@ -39091,49 +39120,51 @@ function PropertyPanelFlat({
39091
39120
  )
39092
39121
  });
39093
39122
  }
39094
- groups.push({
39095
- id: "layout",
39096
- title: "Layout",
39097
- // No scrub accessory: FlatRow/CommitField has no pointer-drag scrubbing
39098
- // (wheel/arrow keys only) advertising "drag values to scrub" here lies.
39099
- summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} \xB7 ${Math.round(displayW)}\xD7${Math.round(displayH)}`,
39100
- content: /* @__PURE__ */ jsx99(
39101
- FlatLayoutSection,
39102
- {
39103
- element,
39104
- styles,
39105
- onSetStyle,
39106
- disabled: !element.capabilities.canEditStyles,
39107
- displayX,
39108
- displayY,
39109
- displayW,
39110
- displayH,
39111
- displayR,
39112
- manualOffsetEditingDisabled,
39113
- manualSizeEditingDisabled,
39114
- manualRotationEditingDisabled,
39115
- commitManualOffset,
39116
- commitManualSize,
39117
- commitManualRotation,
39118
- gsapAnimId,
39119
- navKeyframes,
39120
- currentPct,
39121
- seekFromKfPct,
39122
- animIdForProp,
39123
- resolveAnimIdForProp: animIdForProp,
39124
- gsapRuntimeValues,
39125
- gsapKeyframes: navKeyframes,
39126
- elStart,
39127
- elDuration,
39128
- onCommitAnimatedProperty,
39129
- onCommitAnimatedProperties,
39130
- onSeekToTime,
39131
- onRemoveKeyframe,
39132
- onConvertToKeyframes,
39133
- onLivePreviewProps: createGsapLivePreview(previewIframeRef ?? { current: null })
39134
- }
39135
- )
39136
- });
39123
+ if (sections.layout) {
39124
+ groups.push({
39125
+ id: "layout",
39126
+ title: "Layout",
39127
+ // No scrub accessory: FlatRow/CommitField has no pointer-drag scrubbing
39128
+ // (wheel/arrow keys only) advertising "drag values to scrub" here lies.
39129
+ summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} \xB7 ${Math.round(displayW)}\xD7${Math.round(displayH)}`,
39130
+ content: /* @__PURE__ */ jsx99(
39131
+ FlatLayoutSection,
39132
+ {
39133
+ element,
39134
+ styles,
39135
+ onSetStyle,
39136
+ disabled: !element.capabilities.canEditStyles,
39137
+ displayX,
39138
+ displayY,
39139
+ displayW,
39140
+ displayH,
39141
+ displayR,
39142
+ manualOffsetEditingDisabled,
39143
+ manualSizeEditingDisabled,
39144
+ manualRotationEditingDisabled,
39145
+ commitManualOffset,
39146
+ commitManualSize,
39147
+ commitManualRotation,
39148
+ gsapAnimId,
39149
+ navKeyframes,
39150
+ currentPct,
39151
+ seekFromKfPct,
39152
+ animIdForProp,
39153
+ resolveAnimIdForProp: animIdForProp,
39154
+ gsapRuntimeValues,
39155
+ gsapKeyframes: navKeyframes,
39156
+ elStart,
39157
+ elDuration,
39158
+ onCommitAnimatedProperty,
39159
+ onCommitAnimatedProperties,
39160
+ onSeekToTime,
39161
+ onRemoveKeyframe,
39162
+ onConvertToKeyframes,
39163
+ onLivePreviewProps: createGsapLivePreview(previewIframeRef ?? { current: null })
39164
+ }
39165
+ )
39166
+ });
39167
+ }
39137
39168
  if (showMotionGroup) {
39138
39169
  groups.push({
39139
39170
  id: "motion",
@@ -39600,8 +39631,8 @@ var PropertyPanel = memo31(function PropertyPanel2(props) {
39600
39631
  const manualSizeEditingDisabled = !element.capabilities.canApplyManualSize;
39601
39632
  const manualRotationEditingDisabled = !element.capabilities.canApplyManualRotation;
39602
39633
  const sourceLabel = element.id ? `#${element.id}` : element.selector ?? "";
39603
- const showEditableSections = element.capabilities.canEditStyles;
39604
39634
  const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
39635
+ const showEditableSections = element.capabilities.canEditStyles && sections.style;
39605
39636
  const manualOffset = readStudioPathOffset(element.element);
39606
39637
  const manualSize = readStudioBoxSize(element.element);
39607
39638
  const resolvedWidth = manualSize.width > 0 ? manualSize.width : parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width;
@@ -39765,7 +39796,7 @@ var PropertyPanel = memo31(function PropertyPanel2(props) {
39765
39796
  onRemoveBackground
39766
39797
  }
39767
39798
  ),
39768
- /* @__PURE__ */ jsxs89(Section, { title: "Layout", icon: /* @__PURE__ */ jsx102(Move, { size: 15 }), children: [
39799
+ sections.layout && /* @__PURE__ */ jsxs89(Section, { title: "Layout", icon: /* @__PURE__ */ jsx102(Move, { size: 15 }), children: [
39769
39800
  /* @__PURE__ */ jsxs89("div", { className: RESPONSIVE_GRID, children: [
39770
39801
  /* @__PURE__ */ jsxs89("div", { className: "flex items-center gap-1", children: [
39771
39802
  /* @__PURE__ */ jsx102("div", { className: "flex-1", children: /* @__PURE__ */ jsx102(
@@ -40765,7 +40796,7 @@ var FileTree = memo33(function FileTree2({
40765
40796
  });
40766
40797
 
40767
40798
  // src/App.tsx
40768
- import { useState as useState119, useCallback as useCallback141, useRef as useRef119, useMemo as useMemo49, useEffect as useEffect100, useLayoutEffect as useLayoutEffect4 } from "react";
40799
+ import { useState as useState120, useCallback as useCallback142, useRef as useRef120, useMemo as useMemo50, useEffect as useEffect102, useLayoutEffect as useLayoutEffect4 } from "react";
40769
40800
 
40770
40801
  // src/components/renders/useRenderQueue.ts
40771
40802
  import { useState as useState64, useEffect as useEffect52, useCallback as useCallback58, useRef as useRef63, useMemo as useMemo31 } from "react";
@@ -42174,7 +42205,9 @@ function usePanelLayout(initialState2) {
42174
42205
  const trackedSetRightPanelTab = useCallback62(
42175
42206
  (tab) => {
42176
42207
  if (tab === "design" || tab === "layers") {
42177
- setRightInspectorPanes((panes) => ({ ...panes, [tab]: true }));
42208
+ setRightInspectorPanes(
42209
+ STUDIO_FLAT_INSPECTOR_ENABLED ? { design: tab === "design", layers: tab === "layers" } : (panes) => ({ ...panes, [tab]: true })
42210
+ );
42178
42211
  }
42179
42212
  setRightPanelTab(tab);
42180
42213
  trackStudioEvent("tab_switch", { panel: "right_panel", tab });
@@ -42188,6 +42221,9 @@ function usePanelLayout(initialState2) {
42188
42221
  return next;
42189
42222
  });
42190
42223
  }, []);
42224
+ const setExclusiveRightInspectorPane = useCallback62((pane) => {
42225
+ setRightInspectorPanes({ design: pane === "design", layers: pane === "layers" });
42226
+ }, []);
42191
42227
  return {
42192
42228
  leftWidth,
42193
42229
  setLeftWidth,
@@ -42201,6 +42237,7 @@ function usePanelLayout(initialState2) {
42201
42237
  setRightPanelTab: trackedSetRightPanelTab,
42202
42238
  rightInspectorPanes,
42203
42239
  toggleRightInspectorPane,
42240
+ setExclusiveRightInspectorPane,
42204
42241
  toggleLeftSidebar,
42205
42242
  handlePanelResizeStart,
42206
42243
  handlePanelResizeMove,
@@ -42209,7 +42246,24 @@ function usePanelLayout(initialState2) {
42209
42246
  }
42210
42247
 
42211
42248
  // src/hooks/useFileManager.ts
42212
- import { useState as useState70, useCallback as useCallback65, useRef as useRef71 } from "react";
42249
+ import { useState as useState70, useCallback as useCallback65, useMemo as useMemo34, useRef as useRef71 } from "react";
42250
+
42251
+ // src/utils/studioFileVersion.ts
42252
+ async function studioFileContentVersion(content) {
42253
+ const bytes = new TextEncoder().encode(content);
42254
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
42255
+ const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(
42256
+ ""
42257
+ );
42258
+ return `"sha256:${hex}"`;
42259
+ }
42260
+ async function studioExpectedFileVersion(versions, path, expectedContent) {
42261
+ if (expectedContent !== void 0) return studioFileContentVersion(expectedContent);
42262
+ return versions.get(path);
42263
+ }
42264
+ function createStudioWriteToken() {
42265
+ return globalThis.crypto.randomUUID();
42266
+ }
42213
42267
 
42214
42268
  // src/hooks/useFileTree.ts
42215
42269
  import { useState as useState69, useCallback as useCallback63, useEffect as useEffect55, useMemo as useMemo33 } from "react";
@@ -42354,6 +42408,17 @@ function useFileManager({
42354
42408
  const projectIdRef = useRef71(projectId);
42355
42409
  projectIdRef.current = projectId;
42356
42410
  const importedFontAssetsRef = useRef71([]);
42411
+ const fileVersionScope = useMemo34(
42412
+ () => ({ projectId, versions: /* @__PURE__ */ new Map() }),
42413
+ [projectId]
42414
+ );
42415
+ const fileVersions = fileVersionScope.versions;
42416
+ const observeProjectFileVersion = useCallback65(
42417
+ (path, version) => {
42418
+ fileVersions.set(path, version);
42419
+ },
42420
+ [fileVersions]
42421
+ );
42357
42422
  const {
42358
42423
  projectDir,
42359
42424
  fileTree,
@@ -42373,14 +42438,35 @@ function useFileManager({
42373
42438
  if (!response.ok) throw new Error(`Failed to read ${path}`);
42374
42439
  const data = await response.json();
42375
42440
  if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
42441
+ fileVersions.set(path, data.version ?? response.headers.get("etag"));
42376
42442
  return data.content;
42377
42443
  },
42378
- [projectId]
42444
+ [fileVersions, projectId]
42379
42445
  );
42380
42446
  const writeProjectFile = useCallback65(
42381
- async (path, content) => {
42447
+ async (path, content, expectedContent) => {
42382
42448
  if (!projectId) throw new Error("No active project");
42383
42449
  const writeProjectId = projectId;
42450
+ let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
42451
+ if (expectedVersion === void 0) {
42452
+ const preflight = await fetch(
42453
+ `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`
42454
+ );
42455
+ if (preflight.ok) {
42456
+ const data = await preflight.json();
42457
+ throw new StudioFileConflictError({
42458
+ filePath: path,
42459
+ currentVersion: data.version ?? preflight.headers.get("etag"),
42460
+ currentContent: data.content ?? null,
42461
+ attemptedContent: content
42462
+ });
42463
+ } else if (preflight.status === 404) {
42464
+ expectedVersion = null;
42465
+ } else {
42466
+ throw await createStudioSaveHttpError(preflight, `Failed to read ${path} before save`);
42467
+ }
42468
+ }
42469
+ const writeToken = createStudioWriteToken();
42384
42470
  await retryStudioSave(async () => {
42385
42471
  let response;
42386
42472
  try {
@@ -42388,7 +42474,11 @@ function useFileManager({
42388
42474
  `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
42389
42475
  {
42390
42476
  method: "PUT",
42391
- headers: { "Content-Type": "text/plain" },
42477
+ headers: {
42478
+ "Content-Type": "text/plain",
42479
+ "X-Hyperframes-Write-Token": writeToken,
42480
+ ...expectedVersion ? { "If-Match": expectedVersion } : { "If-None-Match": "*" }
42481
+ },
42392
42482
  body: content
42393
42483
  }
42394
42484
  );
@@ -42397,13 +42487,32 @@ function useFileManager({
42397
42487
  cause: error
42398
42488
  });
42399
42489
  }
42490
+ if (response.status === 409) {
42491
+ const conflict = await response.json().catch(() => null);
42492
+ const currentVersion = conflict?.currentVersion ?? null;
42493
+ if (currentVersion && conflict?.currentContent === content) {
42494
+ fileVersions.set(path, currentVersion);
42495
+ return;
42496
+ }
42497
+ throw new StudioFileConflictError({
42498
+ filePath: path,
42499
+ currentVersion,
42500
+ currentContent: conflict?.currentContent ?? null,
42501
+ attemptedContent: content
42502
+ });
42503
+ }
42400
42504
  if (!response.ok) throw await createStudioSaveHttpError(response, `Failed to save ${path}`);
42505
+ const result = await response.json();
42506
+ const version = result.version ?? response.headers.get("etag");
42507
+ if (!version)
42508
+ throw new Error(`Save response for ${path} did not include a content version`);
42509
+ fileVersions.set(path, version);
42401
42510
  });
42402
42511
  if (projectIdRef.current === writeProjectId && editingPathRef.current === path) {
42403
42512
  setEditingFile({ path, content });
42404
42513
  }
42405
42514
  },
42406
- [projectId]
42515
+ [fileVersions, projectId]
42407
42516
  );
42408
42517
  const updateEditingFileContent = useCallback65((path, content) => {
42409
42518
  if (editingPathRef.current === path) {
@@ -42418,9 +42527,10 @@ function useFileManager({
42418
42527
  );
42419
42528
  if (!response.ok) throw new Error(`Failed to read ${path}`);
42420
42529
  const data = await response.json();
42530
+ fileVersions.set(path, data.version ?? response.headers.get("etag"));
42421
42531
  return typeof data.content === "string" ? data.content : "";
42422
42532
  },
42423
- [projectId]
42533
+ [fileVersions, projectId]
42424
42534
  );
42425
42535
  const { saveRafRef, handleContentChange } = useEditorSave({
42426
42536
  editingPathRef,
@@ -42450,13 +42560,14 @@ function useFileManager({
42450
42560
  return r.json();
42451
42561
  }).then((data) => {
42452
42562
  if (data.content != null) {
42563
+ fileVersions.set(path, data.version ?? null);
42453
42564
  setEditingFile({ path, content: data.content });
42454
42565
  }
42455
42566
  }).catch((err) => {
42456
42567
  showToast(err instanceof Error ? err.message : `Failed to load ${path}`, "error");
42457
42568
  });
42458
42569
  },
42459
- [showToast]
42570
+ [fileVersions, showToast]
42460
42571
  );
42461
42572
  const openSourceForSelection = useCallback65(
42462
42573
  (sourceFile, target) => {
@@ -42477,6 +42588,7 @@ function useFileManager({
42477
42588
  }).then((r) => r.json()).then((data) => {
42478
42589
  if (requestId !== revealRequestIdRef.current) return;
42479
42590
  if (data.content != null) {
42591
+ fileVersions.set(sourceFile, data.version ?? null);
42480
42592
  setEditingFile({ path: sourceFile, content: data.content });
42481
42593
  const match = findTagByTarget(data.content, target);
42482
42594
  setRevealSourceOffset(match ? match.start : null);
@@ -42484,7 +42596,7 @@ function useFileManager({
42484
42596
  }).catch(() => {
42485
42597
  });
42486
42598
  },
42487
- [editingFile?.content]
42599
+ [editingFile?.content, fileVersions]
42488
42600
  );
42489
42601
  const uploadProjectFiles = useCallback65(
42490
42602
  async (files, dir) => {
@@ -42689,6 +42801,7 @@ function useFileManager({
42689
42801
  readProjectFile,
42690
42802
  writeProjectFile,
42691
42803
  readOptionalProjectFile,
42804
+ observeProjectFileVersion,
42692
42805
  updateEditingFileContent,
42693
42806
  // Click-to-source
42694
42807
  revealSourceOffset,
@@ -42751,7 +42864,8 @@ function createDomEditSaveQueue(options = {}) {
42751
42864
  return result;
42752
42865
  } catch (error) {
42753
42866
  consecutiveFailures += 1;
42754
- if (consecutiveFailures >= failureThreshold) open(error);
42867
+ if (getStudioSaveStatusCode(error) === 409 || consecutiveFailures >= failureThreshold)
42868
+ open(error);
42755
42869
  throw error;
42756
42870
  }
42757
42871
  };
@@ -42991,7 +43105,7 @@ function usePreviewPersistence({
42991
43105
  if (!domEditSaveQueueRef.current) {
42992
43106
  domEditSaveQueueRef.current = createDomEditSaveQueue({
42993
43107
  onOpen: (event) => {
42994
- const message = "Auto-save is paused. Check your connection.";
43108
+ const message = event.statusCode === 409 ? "Save paused: this file changed elsewhere. Reload and review the latest version before reapplying your edit." : "Auto-save is paused. Check your connection.";
42995
43109
  setDomEditSaveQueuePaused(message);
42996
43110
  showToastRef.current(message, "error");
42997
43111
  trackStudioEvent("save_queue_paused", {
@@ -43136,7 +43250,10 @@ async function splitHtmlElement(projectId, targetPath, patchTarget, splitTime, n
43136
43250
  }
43137
43251
  );
43138
43252
  if (!response.ok) throw new Error("Split request failed");
43139
- return await response.json();
43253
+ const data = await response.json();
43254
+ const version = data.version ?? response.headers.get("etag");
43255
+ if (!version) throw new Error("Split response did not include a content version");
43256
+ return { ...data, version };
43140
43257
  }
43141
43258
  async function splitGsapAnimations(projectId, targetPath, originalId, newId, splitTime, elementStart, elementDuration) {
43142
43259
  const response = await fetch(
@@ -43164,6 +43281,7 @@ async function splitGsapAnimations(projectId, targetPath, originalId, newId, spl
43164
43281
  const data = await response.json();
43165
43282
  return {
43166
43283
  content: data.ok && data.after ? data.after : null,
43284
+ version: data.version ?? response.headers.get("etag") ?? void 0,
43167
43285
  skippedSelectors: data.skippedSelectors
43168
43286
  };
43169
43287
  }
@@ -43174,9 +43292,9 @@ function getOriginalContent(originals, path) {
43174
43292
  }
43175
43293
  return original;
43176
43294
  }
43177
- async function restoreFilesToOriginal(originals, paths, writeProjectFile) {
43178
- for (const path of paths) {
43179
- await writeProjectFile(path, getOriginalContent(originals, path));
43295
+ async function restoreFilesToOriginal(originals, snapshots, writeProjectFile) {
43296
+ for (const [path, snapshot] of snapshots) {
43297
+ await writeProjectFile(path, getOriginalContent(originals, path), snapshot.after);
43180
43298
  }
43181
43299
  }
43182
43300
  async function readOriginalFiles(pid, elements, activeCompPath) {
@@ -43189,21 +43307,28 @@ async function readOriginalFiles(pid, elements, activeCompPath) {
43189
43307
  }
43190
43308
  return originals;
43191
43309
  }
43192
- async function splitElementsAtTime(pid, elements, splitTime, activeCompPath, originals, snapshots, writeProjectFile) {
43310
+ async function splitElementsAtTime(pid, elements, splitTime, activeCompPath, originals, snapshots, writeProjectFile, observeProjectFileVersion) {
43193
43311
  let count = 0;
43194
43312
  for (const element of elements) {
43195
- const result = await executeSplit(pid, element, splitTime, activeCompPath, writeProjectFile);
43313
+ const result = await executeSplit(
43314
+ pid,
43315
+ element,
43316
+ splitTime,
43317
+ activeCompPath,
43318
+ writeProjectFile,
43319
+ observeProjectFileVersion
43320
+ );
43196
43321
  if (!result.changed) continue;
43197
43322
  snapshots.set(result.targetPath, {
43198
43323
  before: getOriginalContent(originals, result.targetPath),
43199
43324
  after: result.patchedContent
43200
43325
  });
43201
- await writeProjectFile(result.targetPath, result.patchedContent);
43326
+ await writeProjectFile(result.targetPath, result.patchedContent, result.patchedContent);
43202
43327
  count++;
43203
43328
  }
43204
43329
  return count;
43205
43330
  }
43206
- async function executeSplit(pid, element, splitTime, activeCompPath, writeProjectFile) {
43331
+ async function executeSplit(pid, element, splitTime, activeCompPath, writeProjectFile, observeProjectFileVersion) {
43207
43332
  const patchTarget = buildPatchTarget(element);
43208
43333
  if (!patchTarget) throw new Error("Clip is missing a patchable target.");
43209
43334
  const targetPath = element.sourceFile || activeCompPath || "index.html";
@@ -43225,6 +43350,7 @@ async function executeSplit(pid, element, splitTime, activeCompPath, writeProjec
43225
43350
  if (!splitResult.changed) {
43226
43351
  return { targetPath, originalContent, patchedContent: originalContent, changed: false };
43227
43352
  }
43353
+ observeProjectFileVersion?.(targetPath, splitResult.version);
43228
43354
  let patchedContent = typeof splitResult.content === "string" ? splitResult.content : originalContent;
43229
43355
  let skippedSelectors;
43230
43356
  if (element.domId) {
@@ -43239,9 +43365,10 @@ async function executeSplit(pid, element, splitTime, activeCompPath, writeProjec
43239
43365
  element.duration
43240
43366
  );
43241
43367
  if (gsapResult.content) patchedContent = gsapResult.content;
43368
+ if (gsapResult.version) observeProjectFileVersion?.(targetPath, gsapResult.version);
43242
43369
  if (gsapResult.skippedSelectors?.length) skippedSelectors = gsapResult.skippedSelectors;
43243
43370
  } catch (gsapError) {
43244
- await writeProjectFile(targetPath, originalContent);
43371
+ await writeProjectFile(targetPath, originalContent, patchedContent);
43245
43372
  throw gsapError;
43246
43373
  }
43247
43374
  }
@@ -43252,6 +43379,7 @@ function useRazorSplit({
43252
43379
  activeCompPath,
43253
43380
  showToast,
43254
43381
  writeProjectFile,
43382
+ observeProjectFileVersion,
43255
43383
  recordEdit,
43256
43384
  domEditSaveTimestampRef,
43257
43385
  reloadPreview,
@@ -43270,7 +43398,14 @@ function useRazorSplit({
43270
43398
  const pid = projectIdRef.current;
43271
43399
  if (!pid || !canSplitElementAt(element, splitTime)) return;
43272
43400
  try {
43273
- const { targetPath, originalContent, patchedContent, changed, skippedSelectors } = await executeSplit(pid, element, splitTime, activeCompPath, writeProjectFile);
43401
+ const { targetPath, originalContent, patchedContent, changed, skippedSelectors } = await executeSplit(
43402
+ pid,
43403
+ element,
43404
+ splitTime,
43405
+ activeCompPath,
43406
+ writeProjectFile,
43407
+ observeProjectFileVersion
43408
+ );
43274
43409
  if (!changed) {
43275
43410
  showToast("Failed to split clip \u2014 playhead may be outside the clip", "error");
43276
43411
  return;
@@ -43305,6 +43440,7 @@ function useRazorSplit({
43305
43440
  recordEdit,
43306
43441
  showToast,
43307
43442
  writeProjectFile,
43443
+ observeProjectFileVersion,
43308
43444
  domEditSaveTimestampRef,
43309
43445
  reloadPreview,
43310
43446
  forceReloadSdkSession,
@@ -43312,6 +43448,7 @@ function useRazorSplit({
43312
43448
  ]
43313
43449
  );
43314
43450
  const handleRazorSplitAll = useCallback68(
43451
+ // fallow-ignore-next-line complexity
43315
43452
  async (splitTime) => {
43316
43453
  if (isRecordingRef?.current) {
43317
43454
  showToast("Cannot edit timeline while recording", "error");
@@ -43333,7 +43470,8 @@ function useRazorSplit({
43333
43470
  activeCompPath,
43334
43471
  originals,
43335
43472
  finalSnapshots,
43336
- writeProjectFile
43473
+ writeProjectFile,
43474
+ observeProjectFileVersion
43337
43475
  );
43338
43476
  if (splitCount === 0) return;
43339
43477
  domEditSaveTimestampRef.current = Date.now();
@@ -43348,7 +43486,7 @@ function useRazorSplit({
43348
43486
  showToast(`Split ${splitCount} clips at ${splitTime.toFixed(2)}s`, "info");
43349
43487
  } catch (error) {
43350
43488
  try {
43351
- await restoreFilesToOriginal(originals, finalSnapshots.keys(), writeProjectFile);
43489
+ await restoreFilesToOriginal(originals, finalSnapshots, writeProjectFile);
43352
43490
  } catch {
43353
43491
  }
43354
43492
  const message = error instanceof Error ? error.message : "Failed to split clips";
@@ -43360,6 +43498,7 @@ function useRazorSplit({
43360
43498
  recordEdit,
43361
43499
  showToast,
43362
43500
  writeProjectFile,
43501
+ observeProjectFileVersion,
43363
43502
  domEditSaveTimestampRef,
43364
43503
  reloadPreview,
43365
43504
  forceReloadSdkSession,
@@ -44059,6 +44198,7 @@ function useTimelineEditing({
44059
44198
  timelineElements,
44060
44199
  showToast,
44061
44200
  writeProjectFile,
44201
+ observeProjectFileVersion,
44062
44202
  recordEdit,
44063
44203
  domEditSaveTimestampRef,
44064
44204
  reloadPreview,
@@ -44440,6 +44580,7 @@ function useTimelineEditing({
44440
44580
  activeCompPath,
44441
44581
  showToast,
44442
44582
  writeProjectFile,
44583
+ observeProjectFileVersion,
44443
44584
  recordEdit,
44444
44585
  domEditSaveTimestampRef,
44445
44586
  reloadPreview,
@@ -46576,7 +46717,7 @@ function useDomEditCommits({
46576
46717
  const preparedContent = options.prepareContent(patchedContent, targetPath);
46577
46718
  if (preparedContent !== patchedContent) {
46578
46719
  try {
46579
- await writeProjectFile(targetPath, preparedContent);
46720
+ await writeProjectFile(targetPath, preparedContent, patchedContent);
46580
46721
  finalContent = preparedContent;
46581
46722
  } catch (error) {
46582
46723
  showToast(
@@ -46854,7 +46995,7 @@ function useGroupCommits(params) {
46854
46995
  }
46855
46996
 
46856
46997
  // src/hooks/useGsapScriptCommits.ts
46857
- import { useCallback as useCallback87, useMemo as useMemo34, useRef as useRef83 } from "react";
46998
+ import { useCallback as useCallback87, useMemo as useMemo35, useRef as useRef83 } from "react";
46858
46999
  import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
46859
47000
 
46860
47001
  // src/hooks/gsapRuntimePatch.ts
@@ -48101,7 +48242,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
48101
48242
  if (!result) return;
48102
48243
  await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, options);
48103
48244
  }, [showToast, finalizeSuccessfulMutation]);
48104
- const commitMutation = useMemo34(() => {
48245
+ const commitMutation = useMemo35(() => {
48105
48246
  const serializeFile = (file, task) => {
48106
48247
  if (writeProjectFile) {
48107
48248
  return serializeStudioFileMutation(writeProjectFile, file, task);
@@ -48166,7 +48307,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
48166
48307
  },
48167
48308
  [activeProjectId]
48168
48309
  );
48169
- const sdkDeps = useMemo34(
48310
+ const sdkDeps = useMemo35(
48170
48311
  () => writeProjectFile ? {
48171
48312
  editHistory: { recordEdit: editHistory.recordEdit },
48172
48313
  writeProjectFile,
@@ -50426,7 +50567,7 @@ function useStudioSdkSessions(projectId, activeCompPath, domEditSaveTimestampRef
50426
50567
  }
50427
50568
 
50428
50569
  // src/hooks/useBlockHandlers.ts
50429
- import { useCallback as useCallback96, useMemo as useMemo35, useRef as useRef89, useState as useState76 } from "react";
50570
+ import { useCallback as useCallback96, useMemo as useMemo36, useRef as useRef89, useState as useState76 } from "react";
50430
50571
 
50431
50572
  // src/utils/blockInstaller.ts
50432
50573
  function getMaxZIndexFromIframe(iframe) {
@@ -50565,7 +50706,7 @@ function useBlockHandlers({
50565
50706
  setRightPanelTab
50566
50707
  }) {
50567
50708
  const [activeBlockParams, setActiveBlockParams] = useState76(null);
50568
- const blockCtx = useMemo35(
50709
+ const blockCtx = useMemo36(
50569
50710
  () => ({
50570
50711
  activeCompPath: blockCtxDeps.activeCompPath,
50571
50712
  timelineElements: blockCtxDeps.timelineElements,
@@ -50678,8 +50819,20 @@ function useBlockHandlers({
50678
50819
  };
50679
50820
  }
50680
50821
 
50822
+ // src/hooks/useAddAssetAtPlayhead.ts
50823
+ import { useCallback as useCallback97 } from "react";
50824
+ function useAddAssetAtPlayhead(handleTimelineAssetDrop) {
50825
+ return useCallback97(
50826
+ (assetPath) => handleTimelineAssetDrop(assetPath, {
50827
+ start: usePlayerStore.getState().currentTime,
50828
+ track: 0
50829
+ }),
50830
+ [handleTimelineAssetDrop]
50831
+ );
50832
+ }
50833
+
50681
50834
  // src/hooks/useAppHotkeys.ts
50682
- import { useCallback as useCallback97, useEffect as useEffect66, useRef as useRef90 } from "react";
50835
+ import { useCallback as useCallback98, useEffect as useEffect66, useRef as useRef90 } from "react";
50683
50836
  function iframeContentWindow(iframe) {
50684
50837
  try {
50685
50838
  return iframe?.contentWindow ?? null;
@@ -50889,22 +51042,22 @@ function useAppHotkeys({
50889
51042
  }) {
50890
51043
  const previewHotkeyWindowRef = useRef90(null);
50891
51044
  const previewHistoryCleanupRef = useRef90(null);
50892
- const readHistoryFile = useCallback97(
51045
+ const readHistoryFile = useCallback98(
50893
51046
  (path) => path === STUDIO_MOTION_PATH ? readOptionalProjectFile(path) : readProjectFile(path),
50894
51047
  [readOptionalProjectFile, readProjectFile]
50895
51048
  );
50896
- const writeHistoryFile = useCallback97(
51049
+ const writeHistoryFile = useCallback98(
50897
51050
  async (path, content) => {
50898
51051
  domEditSaveTimestampRef.current = Date.now();
50899
51052
  await writeProjectFile(path, content);
50900
51053
  },
50901
51054
  [domEditSaveTimestampRef, writeProjectFile]
50902
51055
  );
50903
- const serializeHistoryFiles = useCallback97(
51056
+ const serializeHistoryFiles = useCallback98(
50904
51057
  (paths, task) => serializeStudioFileMutations(writeProjectFile, paths, task),
50905
51058
  [writeProjectFile]
50906
51059
  );
50907
- const applyHistory = useCallback97(
51060
+ const applyHistory = useCallback98(
50908
51061
  async (direction) => {
50909
51062
  if (tryApplyBeatHistory(direction, editHistory.state, showToast)) return;
50910
51063
  await waitForPendingDomEditSaves();
@@ -50942,8 +51095,8 @@ function useAppHotkeys({
50942
51095
  forceReloadSdkSession
50943
51096
  ]
50944
51097
  );
50945
- const handleUndo = useCallback97(() => applyHistory("undo"), [applyHistory]);
50946
- const handleRedo = useCallback97(() => applyHistory("redo"), [applyHistory]);
51098
+ const handleUndo = useCallback98(() => applyHistory("undo"), [applyHistory]);
51099
+ const handleRedo = useCallback98(() => applyHistory("redo"), [applyHistory]);
50947
51100
  const cbRef = useRef90(null);
50948
51101
  cbRef.current = {
50949
51102
  handleTimelineElementDelete,
@@ -50963,7 +51116,7 @@ function useAppHotkeys({
50963
51116
  domEditSelectionRef,
50964
51117
  showToast
50965
51118
  };
50966
- const handleAppKeyDown = useCallback97((event) => {
51119
+ const handleAppKeyDown = useCallback98((event) => {
50967
51120
  const cb = cbRef.current;
50968
51121
  const key = event.key.toLowerCase();
50969
51122
  if (event.metaKey || event.ctrlKey) {
@@ -50976,7 +51129,7 @@ function useAppHotkeys({
50976
51129
  window.addEventListener("keydown", handleAppKeyDown, true);
50977
51130
  return () => window.removeEventListener("keydown", handleAppKeyDown, true);
50978
51131
  }, [handleAppKeyDown]);
50979
- const syncPreviewTimelineHotkey = useCallback97(
51132
+ const syncPreviewTimelineHotkey = useCallback98(
50980
51133
  (iframe) => {
50981
51134
  const nextWindow = iframeContentWindow(iframe);
50982
51135
  if (previewHotkeyWindowRef.current === nextWindow) return;
@@ -51001,7 +51154,7 @@ function useAppHotkeys({
51001
51154
  },
51002
51155
  [handleAppKeyDown]
51003
51156
  );
51004
- const handleHistoryHotkey = useCallback97((event) => {
51157
+ const handleHistoryHotkey = useCallback98((event) => {
51005
51158
  if (!(event.metaKey || event.ctrlKey) || shouldIgnoreHistoryShortcut(event.target)) return;
51006
51159
  handleUndoRedoKey(
51007
51160
  event,
@@ -51009,7 +51162,7 @@ function useAppHotkeys({
51009
51162
  () => void cbRef.current.handleRedo()
51010
51163
  );
51011
51164
  }, []);
51012
- const syncPreviewHistoryHotkey = useCallback97(
51165
+ const syncPreviewHistoryHotkey = useCallback98(
51013
51166
  (iframe) => {
51014
51167
  previewHistoryCleanupRef.current?.();
51015
51168
  previewHistoryCleanupRef.current = null;
@@ -51047,7 +51200,7 @@ function useAppHotkeys({
51047
51200
  }
51048
51201
 
51049
51202
  // src/hooks/useClipboard.ts
51050
- import { useCallback as useCallback98, useRef as useRef91 } from "react";
51203
+ import { useCallback as useCallback99, useRef as useRef91 } from "react";
51051
51204
 
51052
51205
  // src/utils/clipboardPayload.ts
51053
51206
  function insertAsSibling(source, newHtml, selector, selectorIndex) {
@@ -51157,7 +51310,7 @@ function useClipboard({
51157
51310
  const clipboardRef = useRef91(null);
51158
51311
  const projectIdRef = useRef91(projectId);
51159
51312
  projectIdRef.current = projectId;
51160
- const handleCopy = useCallback98(() => {
51313
+ const handleCopy = useCallback99(() => {
51161
51314
  const { selectedElementId, elements } = usePlayerStore.getState();
51162
51315
  if (selectedElementId) {
51163
51316
  const element = elements.find((el) => (el.key ?? el.id) === selectedElementId);
@@ -51211,7 +51364,7 @@ function useClipboard({
51211
51364
  }
51212
51365
  return false;
51213
51366
  }, [activeCompPath, domEditSelectionRef, previewIframeRef, showToast]);
51214
- const handlePaste = useCallback98(async () => {
51367
+ const handlePaste = useCallback99(async () => {
51215
51368
  const payload = clipboardRef.current;
51216
51369
  if (!payload) {
51217
51370
  showToast("Nothing to paste.", "info");
@@ -51267,7 +51420,7 @@ function useClipboard({
51267
51420
  showToast,
51268
51421
  writeProjectFile
51269
51422
  ]);
51270
- const handleCut = useCallback98(async () => {
51423
+ const handleCut = useCallback99(async () => {
51271
51424
  const copied = handleCopy();
51272
51425
  if (!copied) return false;
51273
51426
  const { selectedElementId, elements } = usePlayerStore.getState();
@@ -51629,11 +51782,11 @@ function useCaptionDetection({
51629
51782
  }
51630
51783
 
51631
51784
  // src/hooks/useRenderClipContent.ts
51632
- import { useCallback as useCallback101 } from "react";
51785
+ import { useCallback as useCallback102 } from "react";
51633
51786
  import { createElement as createElement2 } from "react";
51634
51787
 
51635
51788
  // src/player/components/AudioWaveform.tsx
51636
- import { memo as memo34, useRef as useRef92, useState as useState77, useCallback as useCallback99, useEffect as useEffect68 } from "react";
51789
+ import { memo as memo34, useRef as useRef92, useState as useState77, useCallback as useCallback100, useEffect as useEffect68 } from "react";
51637
51790
  import { jsx as jsx112, jsxs as jsxs98 } from "react/jsx-runtime";
51638
51791
  var BAR_W = 2;
51639
51792
  var GAP = 1;
@@ -51710,7 +51863,7 @@ var AudioWaveform = memo34(function AudioWaveform2({
51710
51863
  cancelled = true;
51711
51864
  };
51712
51865
  }, [audioUrl, waveformUrl, cacheKey, peaks]);
51713
- const draw = useCallback99(() => {
51866
+ const draw = useCallback100(() => {
51714
51867
  const container = containerRef.current;
51715
51868
  const barsEl = barsRef.current;
51716
51869
  if (!container || !barsEl || !peaks) return;
@@ -51731,7 +51884,7 @@ var AudioWaveform = memo34(function AudioWaveform2({
51731
51884
  }
51732
51885
  barsEl.innerHTML = html2;
51733
51886
  }, [peaks, trimStartFraction, trimEndFraction]);
51734
- const setContainerRef = useCallback99(
51887
+ const setContainerRef = useCallback100(
51735
51888
  (el) => {
51736
51889
  roRef.current?.disconnect();
51737
51890
  containerRef.current = el;
@@ -51775,7 +51928,7 @@ var AudioWaveform = memo34(function AudioWaveform2({
51775
51928
  });
51776
51929
 
51777
51930
  // src/player/components/ImageThumbnail.tsx
51778
- import { memo as memo35, useRef as useRef93, useState as useState78, useCallback as useCallback100, useEffect as useEffect69 } from "react";
51931
+ import { memo as memo35, useRef as useRef93, useState as useState78, useCallback as useCallback101, useEffect as useEffect69 } from "react";
51779
51932
  import { jsx as jsx113, jsxs as jsxs99 } from "react/jsx-runtime";
51780
51933
  var ImageThumbnail = memo35(function ImageThumbnail2({
51781
51934
  imageSrc,
@@ -51788,7 +51941,7 @@ var ImageThumbnail = memo35(function ImageThumbnail2({
51788
51941
  const [aspect, setAspect] = useState78(16 / 9);
51789
51942
  const ioRef = useRef93(null);
51790
51943
  const roRef = useRef93(null);
51791
- const setContainerRef = useCallback100((el) => {
51944
+ const setContainerRef = useCallback101((el) => {
51792
51945
  ioRef.current?.disconnect();
51793
51946
  roRef.current?.disconnect();
51794
51947
  if (!el) return;
@@ -51941,7 +52094,7 @@ function useRenderClipContent({
51941
52094
  activePreviewUrl,
51942
52095
  effectiveTimelineDuration
51943
52096
  }) {
51944
- return useCallback101(
52097
+ return useCallback102(
51945
52098
  // Pre-existing clip-content dispatcher; reduced by extracting renderAudioClip.
51946
52099
  // fallow-ignore-next-line complexity
51947
52100
  (el, style) => {
@@ -52013,11 +52166,11 @@ function useRenderClipContent({
52013
52166
  }
52014
52167
 
52015
52168
  // src/hooks/useConsoleErrorCapture.ts
52016
- import { useCallback as useCallback102, useEffect as useEffect70, useRef as useRef94, useState as useState79 } from "react";
52169
+ import { useCallback as useCallback103, useEffect as useEffect70, useRef as useRef94, useState as useState79 } from "react";
52017
52170
  function useConsoleErrorCapture(previewIframe) {
52018
52171
  const [consoleErrors, setConsoleErrors] = useState79(null);
52019
52172
  const consoleErrorsRef = useRef94([]);
52020
- const resetErrors = useCallback102(() => {
52173
+ const resetErrors = useCallback103(() => {
52021
52174
  consoleErrorsRef.current = [];
52022
52175
  setConsoleErrors(null);
52023
52176
  }, []);
@@ -52086,7 +52239,7 @@ function useConsoleErrorCapture(previewIframe) {
52086
52239
  }
52087
52240
 
52088
52241
  // src/hooks/useFrameCapture.ts
52089
- import { useState as useState80, useCallback as useCallback103, useRef as useRef95 } from "react";
52242
+ import { useState as useState80, useCallback as useCallback104, useRef as useRef95 } from "react";
52090
52243
 
52091
52244
  // src/utils/projectRouting.ts
52092
52245
  var PROJECT_HASH_PREFIX = "#project/";
@@ -52175,10 +52328,10 @@ function useFrameCapture({
52175
52328
  setCaptureFrameTime(usePlayerStore.getState().currentTime);
52176
52329
  return liveTime.subscribe(setCaptureFrameTime);
52177
52330
  });
52178
- const refreshCaptureFrameTime = useCallback103(() => {
52331
+ const refreshCaptureFrameTime = useCallback104(() => {
52179
52332
  setCaptureFrameTime(usePlayerStore.getState().currentTime);
52180
52333
  }, []);
52181
- const handleCaptureFrameClick = useCallback103(
52334
+ const handleCaptureFrameClick = useCallback104(
52182
52335
  async (event) => {
52183
52336
  if (!projectId) return;
52184
52337
  event.preventDefault();
@@ -52255,7 +52408,7 @@ function useFrameCapture({
52255
52408
  }
52256
52409
 
52257
52410
  // src/hooks/useLintModal.ts
52258
- import { useState as useState81, useCallback as useCallback104, useEffect as useEffect71, useRef as useRef96, useMemo as useMemo36 } from "react";
52411
+ import { useState as useState81, useCallback as useCallback105, useEffect as useEffect71, useRef as useRef96, useMemo as useMemo37 } from "react";
52259
52412
  function parseFinding(f) {
52260
52413
  return {
52261
52414
  severity: f.severity === "error" ? "error" : "warning",
@@ -52270,7 +52423,7 @@ function useLintModal(projectId, refreshKey) {
52270
52423
  const [linting, setLinting] = useState81(false);
52271
52424
  const [backgroundFindings, setBackgroundFindings] = useState81([]);
52272
52425
  const autoLintRanRef = useRef96(false);
52273
- const runLint = useCallback104(
52426
+ const runLint = useCallback105(
52274
52427
  async (opts) => {
52275
52428
  if (!projectId) return;
52276
52429
  if (!opts?.background) setLinting(true);
@@ -52295,7 +52448,7 @@ function useLintModal(projectId, refreshKey) {
52295
52448
  },
52296
52449
  [projectId]
52297
52450
  );
52298
- const handleLint = useCallback104(() => runLint(), [runLint]);
52451
+ const handleLint = useCallback105(() => runLint(), [runLint]);
52299
52452
  const prevProjectIdRef = useRef96(projectId);
52300
52453
  useEffect71(() => {
52301
52454
  if (projectId !== prevProjectIdRef.current) {
@@ -52311,8 +52464,8 @@ function useLintModal(projectId, refreshKey) {
52311
52464
  const timer = setTimeout(() => void runLint({ background: true }), 1e3);
52312
52465
  return () => clearTimeout(timer);
52313
52466
  }, [projectId, refreshKey, runLint]);
52314
- const closeLintModal = useCallback104(() => setLintModal(null), []);
52315
- const groupFindings = useCallback104(
52467
+ const closeLintModal = useCallback105(() => setLintModal(null), []);
52468
+ const groupFindings = useCallback105(
52316
52469
  (keyFn) => {
52317
52470
  const map = /* @__PURE__ */ new Map();
52318
52471
  for (const f of backgroundFindings) {
@@ -52327,8 +52480,8 @@ function useLintModal(projectId, refreshKey) {
52327
52480
  },
52328
52481
  [backgroundFindings]
52329
52482
  );
52330
- const findingsByElement = useMemo36(() => groupFindings((f) => f.elementId), [groupFindings]);
52331
- const findingsByFile = useMemo36(() => groupFindings((f) => f.file), [groupFindings]);
52483
+ const findingsByElement = useMemo37(() => groupFindings((f) => f.elementId), [groupFindings]);
52484
+ const findingsByFile = useMemo37(() => groupFindings((f) => f.file), [groupFindings]);
52332
52485
  useEffect71(() => {
52333
52486
  usePlayerStore.getState().setLintFindingsByElement(findingsByElement);
52334
52487
  }, [findingsByElement]);
@@ -52344,7 +52497,7 @@ function useLintModal(projectId, refreshKey) {
52344
52497
  }
52345
52498
 
52346
52499
  // src/hooks/useToast.ts
52347
- import { useState as useState82, useCallback as useCallback105, useRef as useRef97 } from "react";
52500
+ import { useState as useState82, useCallback as useCallback106, useRef as useRef97 } from "react";
52348
52501
  var AUTO_DISMISS_MS2 = 4e3;
52349
52502
  var EXIT_MS = 160;
52350
52503
  var MAX_TOASTS = 3;
@@ -52352,21 +52505,21 @@ var nextToastId = 1;
52352
52505
  function useToast() {
52353
52506
  const [toasts, setToasts] = useState82([]);
52354
52507
  const timersRef = useRef97(/* @__PURE__ */ new Map());
52355
- const clearTimer = useCallback105((id) => {
52508
+ const clearTimer = useCallback106((id) => {
52356
52509
  const timer = timersRef.current.get(id);
52357
52510
  if (timer) {
52358
52511
  clearTimeout(timer);
52359
52512
  timersRef.current.delete(id);
52360
52513
  }
52361
52514
  }, []);
52362
- const removeToast = useCallback105(
52515
+ const removeToast = useCallback106(
52363
52516
  (id) => {
52364
52517
  clearTimer(id);
52365
52518
  setToasts((prev) => prev.filter((t) => t.id !== id));
52366
52519
  },
52367
52520
  [clearTimer]
52368
52521
  );
52369
- const dismissToast = useCallback105(
52522
+ const dismissToast = useCallback106(
52370
52523
  (id) => {
52371
52524
  clearTimer(id);
52372
52525
  setToasts((prev) => prev.map((t) => t.id === id ? { ...t, leaving: true } : t));
@@ -52375,7 +52528,7 @@ function useToast() {
52375
52528
  },
52376
52529
  [clearTimer, removeToast]
52377
52530
  );
52378
- const showToast = useCallback105(
52531
+ const showToast = useCallback106(
52379
52532
  (message, tone = "error") => {
52380
52533
  const id = nextToastId++;
52381
52534
  setToasts((prev) => {
@@ -52401,14 +52554,14 @@ function useToast() {
52401
52554
  }
52402
52555
 
52403
52556
  // src/hooks/useCompositionContentLoader.ts
52404
- import { useCallback as useCallback106 } from "react";
52557
+ import { useCallback as useCallback107 } from "react";
52405
52558
  function useCompositionContentLoader({
52406
52559
  projectId,
52407
52560
  setEditingFile,
52408
52561
  setActiveCompPath,
52409
52562
  showToast
52410
52563
  }) {
52411
- return useCallback106(
52564
+ return useCallback107(
52412
52565
  (comp) => {
52413
52566
  setActiveCompPath(comp.endsWith(".html") ? comp : null);
52414
52567
  setEditingFile({ path: comp, content: null });
@@ -52427,7 +52580,7 @@ function useCompositionContentLoader({
52427
52580
  }
52428
52581
 
52429
52582
  // src/hooks/useStudioUrlState.ts
52430
- import { useCallback as useCallback107, useEffect as useEffect72, useRef as useRef98, useState as useState83 } from "react";
52583
+ import { useCallback as useCallback108, useEffect as useEffect72, useRef as useRef98, useState as useState83 } from "react";
52431
52584
 
52432
52585
  // src/utils/studioUrlState.ts
52433
52586
  var VALID_TABS = ["layers", "design", "renders", "slideshow", "variables"];
@@ -52560,7 +52713,7 @@ function useStudioUrlState({
52560
52713
  const [selectionHydrated, setSelectionHydrated] = useState83(initialState2.selection == null);
52561
52714
  const pendingSelectionRef = useRef98(initialState2.selection);
52562
52715
  const stableTimeRef = useRef98(initialState2.currentTime);
52563
- const buildUrlState = useCallback107(
52716
+ const buildUrlState = useCallback108(
52564
52717
  () => ({
52565
52718
  activeCompPath,
52566
52719
  currentTime: stableTimeRef.current,
@@ -52571,7 +52724,7 @@ function useStudioUrlState({
52571
52724
  }),
52572
52725
  [activeCompPath, domEditSelection, rightCollapsed, rightPanelTab]
52573
52726
  );
52574
- const applyUrlSelection = useCallback107(
52727
+ const applyUrlSelection = useCallback108(
52575
52728
  (selection) => {
52576
52729
  if (!selection) {
52577
52730
  applyDomSelection(null, { revealPanel: false });
@@ -52687,7 +52840,7 @@ function useStudioUrlState({
52687
52840
  }
52688
52841
 
52689
52842
  // src/hooks/useStudioContextValue.ts
52690
- import { useCallback as useCallback108, useMemo as useMemo37, useRef as useRef99, useState as useState84 } from "react";
52843
+ import { useCallback as useCallback109, useMemo as useMemo38, useRef as useRef99, useState as useState84 } from "react";
52691
52844
  function buildStudioContextValue(input) {
52692
52845
  return {
52693
52846
  projectId: input.projectId,
@@ -52712,7 +52865,7 @@ function buildStudioContextValue(input) {
52712
52865
  };
52713
52866
  }
52714
52867
  function useInspectorState(rightPanelTab, rightInspectorPanes, rightCollapsed, isPlaying, isGestureRecording) {
52715
- return useMemo37(() => {
52868
+ return useMemo38(() => {
52716
52869
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
52717
52870
  const layersPanelActive = STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.layers;
52718
52871
  const designPanelActive = STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.design;
@@ -52733,21 +52886,21 @@ function useInspectorState(rightPanelTab, rightInspectorPanes, rightCollapsed, i
52733
52886
  function useDragOverlay(onImportFiles) {
52734
52887
  const [active, setActive] = useState84(false);
52735
52888
  const counterRef = useRef99(0);
52736
- const onDragOver = useCallback108((e) => {
52889
+ const onDragOver = useCallback109((e) => {
52737
52890
  if (!e.dataTransfer.types.includes("Files")) return;
52738
52891
  e.preventDefault();
52739
52892
  }, []);
52740
- const onDragEnter = useCallback108((e) => {
52893
+ const onDragEnter = useCallback109((e) => {
52741
52894
  if (!e.dataTransfer.types.includes("Files")) return;
52742
52895
  e.preventDefault();
52743
52896
  counterRef.current++;
52744
52897
  setActive(true);
52745
52898
  }, []);
52746
- const onDragLeave = useCallback108(() => {
52899
+ const onDragLeave = useCallback109(() => {
52747
52900
  counterRef.current--;
52748
52901
  if (counterRef.current === 0) setActive(false);
52749
52902
  }, []);
52750
- const onDrop = useCallback108(
52903
+ const onDrop = useCallback109(
52751
52904
  (e) => {
52752
52905
  counterRef.current = 0;
52753
52906
  setActive(false);
@@ -52760,7 +52913,7 @@ function useDragOverlay(onImportFiles) {
52760
52913
  return { active, onDragOver, onDragEnter, onDragLeave, onDrop };
52761
52914
  }
52762
52915
  function useGlobalFileDrop(handleTimelineFileDrop) {
52763
- const onDrop = useCallback108(
52916
+ const onDrop = useCallback109(
52764
52917
  (files) => {
52765
52918
  const start = usePlayerStore.getState().currentTime;
52766
52919
  void handleTimelineFileDrop(Array.from(files), { start, track: 0 });
@@ -52771,10 +52924,10 @@ function useGlobalFileDrop(handleTimelineFileDrop) {
52771
52924
  }
52772
52925
 
52773
52926
  // src/components/StudioHeader.tsx
52774
- import { useRef as useRef100 } from "react";
52927
+ import { useRef as useRef101 } from "react";
52775
52928
 
52776
52929
  // src/contexts/PanelLayoutContext.tsx
52777
- import { createContext as createContext8, useContext as useContext8, useMemo as useMemo38 } from "react";
52930
+ import { createContext as createContext8, useContext as useContext8, useMemo as useMemo39 } from "react";
52778
52931
  import { jsx as jsx114 } from "react/jsx-runtime";
52779
52932
  var PanelLayoutContext = createContext8(null);
52780
52933
  function usePanelLayoutContext() {
@@ -52796,6 +52949,7 @@ function PanelLayoutProvider({
52796
52949
  setRightPanelTab,
52797
52950
  rightInspectorPanes,
52798
52951
  toggleRightInspectorPane,
52952
+ setExclusiveRightInspectorPane,
52799
52953
  toggleLeftSidebar,
52800
52954
  handlePanelResizeStart,
52801
52955
  handlePanelResizeMove,
@@ -52803,7 +52957,7 @@ function PanelLayoutProvider({
52803
52957
  },
52804
52958
  children
52805
52959
  }) {
52806
- const stable = useMemo38(
52960
+ const stable = useMemo39(
52807
52961
  () => ({
52808
52962
  leftWidth,
52809
52963
  setLeftWidth,
@@ -52817,6 +52971,7 @@ function PanelLayoutProvider({
52817
52971
  setRightPanelTab,
52818
52972
  rightInspectorPanes,
52819
52973
  toggleRightInspectorPane,
52974
+ setExclusiveRightInspectorPane,
52820
52975
  toggleLeftSidebar,
52821
52976
  handlePanelResizeStart,
52822
52977
  handlePanelResizeMove,
@@ -52835,6 +52990,7 @@ function PanelLayoutProvider({
52835
52990
  setRightPanelTab,
52836
52991
  rightInspectorPanes,
52837
52992
  toggleRightInspectorPane,
52993
+ setExclusiveRightInspectorPane,
52838
52994
  toggleLeftSidebar,
52839
52995
  handlePanelResizeStart,
52840
52996
  handlePanelResizeMove,
@@ -52847,10 +53003,11 @@ function PanelLayoutProvider({
52847
53003
  // src/contexts/ViewModeContext.tsx
52848
53004
  import {
52849
53005
  createContext as createContext9,
52850
- useCallback as useCallback109,
53006
+ useCallback as useCallback110,
52851
53007
  useContext as useContext9,
52852
53008
  useEffect as useEffect73,
52853
- useMemo as useMemo39,
53009
+ useMemo as useMemo40,
53010
+ useRef as useRef100,
52854
53011
  useState as useState85
52855
53012
  } from "react";
52856
53013
  import { jsx as jsx115 } from "react/jsx-runtime";
@@ -52869,18 +53026,59 @@ function writeViewModeToUrl(mode) {
52869
53026
  }
52870
53027
  window.history.replaceState(window.history.state, "", url);
52871
53028
  }
53029
+ function readHistoryLocation() {
53030
+ if (typeof window === "undefined") return null;
53031
+ return {
53032
+ href: window.location.href,
53033
+ state: window.history.state
53034
+ };
53035
+ }
52872
53036
  function useViewModeState() {
52873
53037
  const [viewMode, setMode] = useState85(() => readViewModeFromUrl());
53038
+ const guardsRef = useRef100(/* @__PURE__ */ new Set());
53039
+ const acceptedLocationRef = useRef100(readHistoryLocation());
53040
+ const canSetViewMode = useCallback110((mode) => {
53041
+ for (const guard of guardsRef.current) {
53042
+ if (!guard(mode)) return false;
53043
+ }
53044
+ return true;
53045
+ }, []);
52874
53046
  useEffect73(() => {
52875
- const onPopState = () => setMode(readViewModeFromUrl());
53047
+ const onPopState = () => {
53048
+ const mode = readViewModeFromUrl();
53049
+ if (mode !== viewMode && !canSetViewMode(mode)) {
53050
+ const acceptedLocation = acceptedLocationRef.current;
53051
+ if (acceptedLocation) {
53052
+ window.history.pushState(acceptedLocation.state, "", acceptedLocation.href);
53053
+ }
53054
+ return;
53055
+ }
53056
+ setMode(mode);
53057
+ acceptedLocationRef.current = readHistoryLocation();
53058
+ };
52876
53059
  window.addEventListener("popstate", onPopState);
52877
53060
  return () => window.removeEventListener("popstate", onPopState);
53061
+ }, [canSetViewMode, viewMode]);
53062
+ const setViewMode = useCallback110(
53063
+ (mode) => {
53064
+ if (!canSetViewMode(mode)) return false;
53065
+ setMode(mode);
53066
+ writeViewModeToUrl(mode);
53067
+ acceptedLocationRef.current = readHistoryLocation();
53068
+ return true;
53069
+ },
53070
+ [canSetViewMode]
53071
+ );
53072
+ const registerViewModeGuard = useCallback110((guard) => {
53073
+ guardsRef.current.add(guard);
53074
+ return () => {
53075
+ guardsRef.current.delete(guard);
53076
+ };
52878
53077
  }, []);
52879
- const setViewMode = useCallback109((mode) => {
52880
- setMode(mode);
52881
- writeViewModeToUrl(mode);
52882
- }, []);
52883
- return useMemo39(() => ({ viewMode, setViewMode }), [viewMode, setViewMode]);
53078
+ return useMemo40(
53079
+ () => ({ viewMode, setViewMode, registerViewModeGuard }),
53080
+ [viewMode, setViewMode, registerViewModeGuard]
53081
+ );
52884
53082
  }
52885
53083
  var ViewModeContext = createContext9(null);
52886
53084
  function useViewMode() {
@@ -53085,11 +53283,10 @@ var VIEW_MODE_OPTIONS = [
53085
53283
  ];
53086
53284
  function ViewModeToggle() {
53087
53285
  const { viewMode, setViewMode } = useViewMode();
53088
- const tabRefs = useRef100([]);
53286
+ const tabRefs = useRef101([]);
53089
53287
  const selectMode = (mode) => {
53090
53288
  if (mode === viewMode) return;
53091
- trackStudioEvent("view_mode_toggle", { mode });
53092
- setViewMode(mode);
53289
+ if (setViewMode(mode)) trackStudioEvent("view_mode_toggle", { mode });
53093
53290
  };
53094
53291
  const handleKeyDown = (e, index) => {
53095
53292
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
@@ -53317,10 +53514,10 @@ function StudioHeader({
53317
53514
  }
53318
53515
 
53319
53516
  // src/hooks/useGestureCommit.ts
53320
- import { useState as useState87, useCallback as useCallback111, useRef as useRef102, useEffect as useEffect75 } from "react";
53517
+ import { useState as useState87, useCallback as useCallback112, useRef as useRef103, useEffect as useEffect75 } from "react";
53321
53518
 
53322
53519
  // src/hooks/useGestureRecording.ts
53323
- import { useCallback as useCallback110, useEffect as useEffect74, useRef as useRef101, useState as useState86 } from "react";
53520
+ import { useCallback as useCallback111, useEffect as useEffect74, useRef as useRef102, useState as useState86 } from "react";
53324
53521
  function readBasePosition(element, iframeEl) {
53325
53522
  let baseOpacity = 1;
53326
53523
  let baseScale = 1;
@@ -53439,10 +53636,10 @@ function createRecordingRefs() {
53439
53636
  function useGestureRecording() {
53440
53637
  const [isRecording, setIsRecording] = useState86(false);
53441
53638
  const [recordingDuration, setRecordingDuration] = useState86(0);
53442
- const isRecordingRef = useRef101(false);
53443
- const refs = useRef101(createRecordingRefs());
53444
- const samplesRef = useRef101(refs.current.samples);
53445
- const trailRef = useRef101(refs.current.trail);
53639
+ const isRecordingRef = useRef102(false);
53640
+ const refs = useRef102(createRecordingRefs());
53641
+ const samplesRef = useRef102(refs.current.samples);
53642
+ const trailRef = useRef102(refs.current.trail);
53446
53643
  useEffect74(() => {
53447
53644
  const r = refs.current;
53448
53645
  return () => {
@@ -53451,7 +53648,7 @@ function useGestureRecording() {
53451
53648
  isRecordingRef.current = false;
53452
53649
  };
53453
53650
  }, []);
53454
- const startRecording = useCallback110(
53651
+ const startRecording = useCallback111(
53455
53652
  (element, iframeEl, elementEndTime) => {
53456
53653
  if (isRecordingRef.current) return;
53457
53654
  isRecordingRef.current = true;
@@ -53551,7 +53748,7 @@ function useGestureRecording() {
53551
53748
  []
53552
53749
  // No deps — uses refs only for all mutable state
53553
53750
  );
53554
- const stopRecording = useCallback110(() => {
53751
+ const stopRecording = useCallback111(() => {
53555
53752
  if (!isRecordingRef.current) return [];
53556
53753
  isRecordingRef.current = false;
53557
53754
  const r = refs.current;
@@ -53581,7 +53778,7 @@ function useGestureRecording() {
53581
53778
  setIsRecording(false);
53582
53779
  return frozen;
53583
53780
  }, []);
53584
- const clearSamples = useCallback110(() => {
53781
+ const clearSamples = useCallback111(() => {
53585
53782
  const r = refs.current;
53586
53783
  r.samples = [];
53587
53784
  r.trail = [];
@@ -53838,13 +54035,13 @@ function useGestureCommit({
53838
54035
  }) {
53839
54036
  const gestureRecording = useGestureRecording();
53840
54037
  const [gestureState, setGestureState] = useState87("idle");
53841
- const gestureStateRef = useRef102("idle");
53842
- const recordingAutoStopRef = useRef102(void 0);
53843
- const recordingStartTimeRef = useRef102(0);
53844
- const commitInFlightRef = useRef102(false);
53845
- const capturedSelectionRef = useRef102(null);
54038
+ const gestureStateRef = useRef103("idle");
54039
+ const recordingAutoStopRef = useRef103(void 0);
54040
+ const recordingStartTimeRef = useRef103(0);
54041
+ const commitInFlightRef = useRef103(false);
54042
+ const capturedSelectionRef = useRef103(null);
53846
54043
  useEffect75(() => () => clearInterval(recordingAutoStopRef.current), []);
53847
- const stopAndCommitRecording = useCallback111(async () => {
54044
+ const stopAndCommitRecording = useCallback112(async () => {
53848
54045
  clearInterval(recordingAutoStopRef.current);
53849
54046
  if (commitInFlightRef.current) {
53850
54047
  return;
@@ -54010,7 +54207,7 @@ function useGestureCommit({
54010
54207
  commitInFlightRef.current = false;
54011
54208
  }
54012
54209
  }, [gestureRecording, showToast, isGestureRecordingRef, domEditSessionRef]);
54013
- const handleToggleRecording = useCallback111(() => {
54210
+ const handleToggleRecording = useCallback112(() => {
54014
54211
  if (gestureStateRef.current === "recording") {
54015
54212
  void stopAndCommitRecording();
54016
54213
  return;
@@ -54056,7 +54253,7 @@ function useGestureCommit({
54056
54253
  }
54057
54254
 
54058
54255
  // src/components/editor/GestureTrailOverlay.tsx
54059
- import { memo as memo36, useMemo as useMemo40 } from "react";
54256
+ import { memo as memo36, useMemo as useMemo41 } from "react";
54060
54257
  import { Fragment as Fragment36, jsx as jsx117, jsxs as jsxs101 } from "react/jsx-runtime";
54061
54258
  var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54062
54259
  samples,
@@ -54068,7 +54265,7 @@ var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54068
54265
  mode,
54069
54266
  accentColor = "#3CE6AC"
54070
54267
  }) {
54071
- const trailPoints = useMemo40(() => {
54268
+ const trailPoints = useMemo41(() => {
54072
54269
  if (!canvasRect) return "";
54073
54270
  if (trail && trail.length > 1) {
54074
54271
  return trail.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
@@ -54076,7 +54273,7 @@ var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54076
54273
  if (samples.length === 0) return "";
54077
54274
  return samples.filter((s) => s.properties.x != null && s.properties.y != null).map((s) => `${s.properties.x},${s.properties.y}`).join(" ");
54078
54275
  }, [samples, trail, sampleCount, canvasRect?.left, canvasRect?.top]);
54079
- const simplifiedPath = useMemo40(() => {
54276
+ const simplifiedPath = useMemo41(() => {
54080
54277
  if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
54081
54278
  const pts = [];
54082
54279
  for (const [pct, props] of simplifiedPoints) {
@@ -54088,7 +54285,7 @@ var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54088
54285
  if (pts.length === 0) return "";
54089
54286
  return pts.map((p) => `${p.x},${p.y}`).join(" ");
54090
54287
  }, [simplifiedPoints]);
54091
- const diamondPositions = useMemo40(() => {
54288
+ const diamondPositions = useMemo41(() => {
54092
54289
  if (!simplifiedPoints || simplifiedPoints.size === 0) return [];
54093
54290
  const pts = [];
54094
54291
  for (const [pct, props] of simplifiedPoints) {
@@ -54168,20 +54365,20 @@ var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54168
54365
  });
54169
54366
 
54170
54367
  // src/components/StudioLeftSidebar.tsx
54171
- import { useCallback as useCallback118 } from "react";
54368
+ import { useCallback as useCallback119 } from "react";
54172
54369
 
54173
54370
  // src/components/sidebar/LeftSidebar.tsx
54174
54371
  import {
54175
54372
  memo as memo40,
54176
54373
  useState as useState96,
54177
- useCallback as useCallback117,
54374
+ useCallback as useCallback118,
54178
54375
  useImperativeHandle,
54179
- useRef as useRef108,
54376
+ useRef as useRef109,
54180
54377
  forwardRef as forwardRef3
54181
54378
  } from "react";
54182
54379
 
54183
54380
  // src/components/sidebar/CompositionsTab.tsx
54184
- import { memo as memo37, useCallback as useCallback112, useEffect as useEffect76, useRef as useRef103, useState as useState88 } from "react";
54381
+ import { memo as memo37, useCallback as useCallback113, useEffect as useEffect76, useRef as useRef104, useState as useState88 } from "react";
54185
54382
  import { jsx as jsx118, jsxs as jsxs102 } from "react/jsx-runtime";
54186
54383
  var DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
54187
54384
  var CARD_W = 80;
@@ -54250,10 +54447,10 @@ function CompCard({
54250
54447
  }) {
54251
54448
  const [hovered, setHovered] = useState88(false);
54252
54449
  const [stageSize, setStageSize] = useState88(DEFAULT_PREVIEW_STAGE);
54253
- const iframeRef = useRef103(null);
54254
- const hoverTimer = useRef103(null);
54255
- const syncTimer = useRef103(null);
54256
- const requestIframePlaybackSync = useCallback112((shouldPlay) => {
54450
+ const iframeRef = useRef104(null);
54451
+ const hoverTimer = useRef104(null);
54452
+ const syncTimer = useRef104(null);
54453
+ const requestIframePlaybackSync = useCallback113((shouldPlay) => {
54257
54454
  if (syncTimer.current) {
54258
54455
  clearTimeout(syncTimer.current);
54259
54456
  syncTimer.current = null;
@@ -54411,7 +54608,7 @@ var CompositionsTab = memo37(function CompositionsTab2({
54411
54608
  });
54412
54609
 
54413
54610
  // src/components/sidebar/AssetsTab.tsx
54414
- import { memo as memo38, useState as useState93, useCallback as useCallback115, useRef as useRef106, useMemo as useMemo42, useEffect as useEffect81 } from "react";
54611
+ import { memo as memo38, useState as useState93, useCallback as useCallback116, useRef as useRef107, useMemo as useMemo43, useEffect as useEffect81 } from "react";
54415
54612
 
54416
54613
  // src/components/sidebar/assetHelpers.ts
54417
54614
  function getCategory(path) {
@@ -54461,7 +54658,7 @@ var CATEGORY_LABELS = {
54461
54658
  var FILTER_ORDER = ["audio", "images", "video", "fonts"];
54462
54659
 
54463
54660
  // src/components/sidebar/AudioRow.tsx
54464
- import { useState as useState89, useRef as useRef104, useEffect as useEffect77, useCallback as useCallback113 } from "react";
54661
+ import { useState as useState89, useRef as useRef105, useEffect as useEffect77, useCallback as useCallback114 } from "react";
54465
54662
 
54466
54663
  // src/components/sidebar/AssetContextMenu.tsx
54467
54664
  import { jsx as jsx119, jsxs as jsxs103 } from "react/jsx-runtime";
@@ -54590,24 +54787,24 @@ function AudioRow({
54590
54787
  const [contextMenu, setContextMenu] = useState89(null);
54591
54788
  const [playing, setPlaying] = useState89(false);
54592
54789
  const [bars, setBars] = useState89([]);
54593
- const audioRef = useRef104(null);
54594
- const actxRef = useRef104(null);
54595
- const analyserRef = useRef104(null);
54596
- const sourceRef = useRef104(null);
54597
- const animRef = useRef104(0);
54790
+ const audioRef = useRef105(null);
54791
+ const actxRef = useRef105(null);
54792
+ const analyserRef = useRef105(null);
54793
+ const sourceRef = useRef105(null);
54794
+ const animRef = useRef105(0);
54598
54795
  const name = basename2(asset);
54599
54796
  const subtype = getAudioSubtype(asset);
54600
54797
  const serveUrl = resolveMediaPreviewUrl(asset, projectId);
54601
- const pointerDownRef = useRef104(null);
54798
+ const pointerDownRef = useRef105(null);
54602
54799
  const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
54603
54800
  const requestClipReveal = usePlayerStore((s) => s.requestClipReveal);
54604
54801
  const elements = usePlayerStore((s) => s.elements);
54605
54802
  const setPreviewAsset = useAssetPreviewStore((s) => s.setPreviewAsset);
54606
54803
  const clearPreviewAsset = useAssetPreviewStore((s) => s.clearPreviewAsset);
54607
- const handlePointerDown = useCallback113((e) => {
54804
+ const handlePointerDown = useCallback114((e) => {
54608
54805
  pointerDownRef.current = { x: e.clientX, y: e.clientY };
54609
54806
  }, []);
54610
- const handlePointerUp = useCallback113(
54807
+ const handlePointerUp = useCallback114(
54611
54808
  (e) => {
54612
54809
  const origin = pointerDownRef.current;
54613
54810
  pointerDownRef.current = null;
@@ -54671,7 +54868,7 @@ function AudioRow({
54671
54868
  }
54672
54869
  return () => cancelAnimationFrame(animRef.current);
54673
54870
  }, [playing]);
54674
- const togglePlay = useCallback113(async () => {
54871
+ const togglePlay = useCallback114(async () => {
54675
54872
  if (playing) {
54676
54873
  audioRef.current?.pause();
54677
54874
  setPlaying(false);
@@ -54781,7 +54978,7 @@ function AudioRow({
54781
54978
  }
54782
54979
 
54783
54980
  // src/components/sidebar/GlobalAssetsView.tsx
54784
- import { useEffect as useEffect78, useMemo as useMemo41, useState as useState90 } from "react";
54981
+ import { useEffect as useEffect78, useMemo as useMemo42, useState as useState90 } from "react";
54785
54982
  import { jsx as jsx121, jsxs as jsxs105 } from "react/jsx-runtime";
54786
54983
  function globalAssetRows(records, query = "") {
54787
54984
  const q = query.trim().toLowerCase();
@@ -54808,7 +55005,7 @@ function GlobalAssetsView({ searchQuery }) {
54808
55005
  cancelled = true;
54809
55006
  };
54810
55007
  }, []);
54811
- const rows = useMemo41(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
55008
+ const rows = useMemo42(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
54812
55009
  if (records === null) {
54813
55010
  return /* @__PURE__ */ jsx121("p", { className: "px-4 py-3 text-[11px] text-panel-text-5", children: "Loading global assets\u2026" });
54814
55011
  }
@@ -54840,7 +55037,7 @@ function GlobalAssetsView({ searchQuery }) {
54840
55037
  }
54841
55038
 
54842
55039
  // src/components/sidebar/AssetCard.tsx
54843
- import { useState as useState92, useEffect as useEffect80, useRef as useRef105, useCallback as useCallback114 } from "react";
55040
+ import { useState as useState92, useEffect as useEffect80, useRef as useRef106, useCallback as useCallback115 } from "react";
54844
55041
 
54845
55042
  // src/components/ui/VideoFrameThumbnail.tsx
54846
55043
  import { useState as useState91, useEffect as useEffect79 } from "react";
@@ -54965,16 +55162,16 @@ function AssetCard({
54965
55162
  const probedDuration = useProbedDuration(serveUrl, !isVideo || duration != null);
54966
55163
  const resolvedDuration = duration ?? probedDuration ?? void 0;
54967
55164
  const durationLabel = formatDuration(resolvedDuration ?? 0);
54968
- const pointerDownRef = useRef105(null);
55165
+ const pointerDownRef = useRef106(null);
54969
55166
  const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
54970
55167
  const requestClipReveal = usePlayerStore((s) => s.requestClipReveal);
54971
55168
  const elements = usePlayerStore((s) => s.elements);
54972
55169
  const setPreviewAsset = useAssetPreviewStore((s) => s.setPreviewAsset);
54973
55170
  const clearPreviewAsset = useAssetPreviewStore((s) => s.clearPreviewAsset);
54974
- const handlePointerDown = useCallback114((e) => {
55171
+ const handlePointerDown = useCallback115((e) => {
54975
55172
  pointerDownRef.current = { x: e.clientX, y: e.clientY };
54976
55173
  }, []);
54977
- const handlePointerUp = useCallback114(
55174
+ const handlePointerUp = useCallback115(
54978
55175
  (e) => {
54979
55176
  const origin = pointerDownRef.current;
54980
55177
  pointerDownRef.current = null;
@@ -55167,7 +55364,7 @@ var AssetsTab = memo38(function AssetsTab2({
55167
55364
  onRename,
55168
55365
  onAddAssetToTimeline
55169
55366
  }) {
55170
- const fileInputRef = useRef106(null);
55367
+ const fileInputRef = useRef107(null);
55171
55368
  const [dragOver, setDragOver] = useState93(false);
55172
55369
  const [copiedPath, setCopiedPath] = useState93(null);
55173
55370
  const [activeFilter, setActiveFilter] = useState93("all");
@@ -55175,7 +55372,7 @@ var AssetsTab = memo38(function AssetsTab2({
55175
55372
  const [searchQuery, setSearchQuery] = useState93("");
55176
55373
  const [viewMode, setViewMode] = useState93("local");
55177
55374
  const [manifest, setManifest] = useState93(/* @__PURE__ */ new Map());
55178
- const manifest404Ref = useRef106(/* @__PURE__ */ new Set());
55375
+ const manifest404Ref = useRef107(/* @__PURE__ */ new Set());
55179
55376
  const assetsKey = assets.join("|");
55180
55377
  useEffect81(() => {
55181
55378
  if (manifest404Ref.current.has(projectId)) return;
@@ -55204,7 +55401,7 @@ var AssetsTab = memo38(function AssetsTab2({
55204
55401
  cancelled = true;
55205
55402
  };
55206
55403
  }, [projectId, assetsKey]);
55207
- const handleDrop = useCallback115(
55404
+ const handleDrop = useCallback116(
55208
55405
  (e) => {
55209
55406
  e.preventDefault();
55210
55407
  setDragOver(false);
@@ -55212,7 +55409,7 @@ var AssetsTab = memo38(function AssetsTab2({
55212
55409
  },
55213
55410
  [onImport]
55214
55411
  );
55215
- const handleCopyPath = useCallback115(async (path) => {
55412
+ const handleCopyPath = useCallback116(async (path) => {
55216
55413
  const copied = await copyTextToClipboard(path);
55217
55414
  if (copied) {
55218
55415
  setCopiedPath(path);
@@ -55220,8 +55417,8 @@ var AssetsTab = memo38(function AssetsTab2({
55220
55417
  }
55221
55418
  }, []);
55222
55419
  const elements = usePlayerStore((s) => s.elements);
55223
- const usedPaths = useMemo42(() => deriveUsedPaths(elements), [elements]);
55224
- const mediaAssets = useMemo42(() => {
55420
+ const usedPaths = useMemo43(() => deriveUsedPaths(elements), [elements]);
55421
+ const mediaAssets = useMemo43(() => {
55225
55422
  const media = assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a));
55226
55423
  const all = filterByUsage(media, usedPaths, usageFilter);
55227
55424
  if (!searchQuery) return all;
@@ -55233,7 +55430,7 @@ var AssetsTab = memo38(function AssetsTab2({
55233
55430
  return rec?.description?.toLowerCase().includes(q);
55234
55431
  });
55235
55432
  }, [assets, searchQuery, manifest, usageFilter, usedPaths]);
55236
- const categorized = useMemo42(() => {
55433
+ const categorized = useMemo43(() => {
55237
55434
  const groups = { audio: [], images: [], video: [], fonts: [] };
55238
55435
  for (const a of mediaAssets) {
55239
55436
  const cat = getCategory(a);
@@ -55248,12 +55445,12 @@ var AssetsTab = memo38(function AssetsTab2({
55248
55445
  }
55249
55446
  return groups;
55250
55447
  }, [mediaAssets, usedPaths]);
55251
- const counts = useMemo42(() => {
55448
+ const counts = useMemo43(() => {
55252
55449
  const c = { all: mediaAssets.length };
55253
55450
  for (const cat of FILTER_ORDER) c[cat] = categorized[cat].length;
55254
55451
  return c;
55255
55452
  }, [mediaAssets, categorized]);
55256
- const usageCounts = useMemo42(
55453
+ const usageCounts = useMemo43(
55257
55454
  () => countUsage(
55258
55455
  assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
55259
55456
  usedPaths
@@ -55496,11 +55693,11 @@ var AssetsTab = memo38(function AssetsTab2({
55496
55693
  });
55497
55694
 
55498
55695
  // src/components/sidebar/BlocksTab.tsx
55499
- import { memo as memo39, useState as useState95, useCallback as useCallback116, useRef as useRef107, useEffect as useEffect83 } from "react";
55696
+ import { memo as memo39, useState as useState95, useCallback as useCallback117, useRef as useRef108, useEffect as useEffect83 } from "react";
55500
55697
  import { createPortal as createPortal7 } from "react-dom";
55501
55698
 
55502
55699
  // src/hooks/useBlockCatalog.ts
55503
- import { useState as useState94, useEffect as useEffect82, useMemo as useMemo43 } from "react";
55700
+ import { useState as useState94, useEffect as useEffect82, useMemo as useMemo44 } from "react";
55504
55701
 
55505
55702
  // src/utils/blockCategories.ts
55506
55703
  import {
@@ -55554,7 +55751,7 @@ function useBlockCatalog() {
55554
55751
  cancelled = true;
55555
55752
  };
55556
55753
  }, []);
55557
- const filteredBlocks = useMemo43(() => {
55754
+ const filteredBlocks = useMemo44(() => {
55558
55755
  let result = blocks;
55559
55756
  if (category) {
55560
55757
  result = result.filter((b) => b.category === category);
@@ -55790,16 +55987,16 @@ function BlockCard({
55790
55987
  }) {
55791
55988
  const [hovered, setHovered] = useState95(false);
55792
55989
  const [adding, setAdding] = useState95(false);
55793
- const hoverTimer = useRef107(null);
55990
+ const hoverTimer = useRef108(null);
55794
55991
  const colors = getCategoryColors(category);
55795
55992
  const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl");
55796
- const handleEnter = useCallback116(() => {
55993
+ const handleEnter = useCallback117(() => {
55797
55994
  hoverTimer.current = setTimeout(() => {
55798
55995
  setHovered(true);
55799
55996
  onPreview?.({ videoUrl, posterUrl, title });
55800
55997
  }, 300);
55801
55998
  }, [onPreview, videoUrl, posterUrl, title]);
55802
- const handleLeave = useCallback116(() => {
55999
+ const handleLeave = useCallback117(() => {
55803
56000
  if (hoverTimer.current) {
55804
56001
  clearTimeout(hoverTimer.current);
55805
56002
  hoverTimer.current = null;
@@ -55812,7 +56009,7 @@ function BlockCard({
55812
56009
  if (hoverTimer.current) clearTimeout(hoverTimer.current);
55813
56010
  };
55814
56011
  }, []);
55815
- const handleAdd = useCallback116(
56012
+ const handleAdd = useCallback117(
55816
56013
  (e) => {
55817
56014
  e.stopPropagation();
55818
56015
  if (adding || !onAdd) return;
@@ -55823,7 +56020,7 @@ function BlockCard({
55823
56020
  [onAdd, adding]
55824
56021
  );
55825
56022
  const { activeCompPath, compositionDimensions } = useStudioShellContext();
55826
- const handleShowPrompt = useCallback116(
56023
+ const handleShowPrompt = useCallback117(
55827
56024
  (e) => {
55828
56025
  e.stopPropagation();
55829
56026
  const state = usePlayerStore.getState();
@@ -55968,11 +56165,11 @@ function PromptPreviewModal({
55968
56165
  }) {
55969
56166
  const [value, setValue] = useState95(prompt);
55970
56167
  const [copied, setCopied] = useState95(false);
55971
- const textareaRef = useRef107(null);
56168
+ const textareaRef = useRef108(null);
55972
56169
  useEffect83(() => {
55973
56170
  requestAnimationFrame(() => textareaRef.current?.focus());
55974
56171
  }, []);
55975
- const handleCopy = useCallback116(() => {
56172
+ const handleCopy = useCallback117(() => {
55976
56173
  navigator.clipboard.writeText(value);
55977
56174
  setCopied(true);
55978
56175
  setTimeout(() => setCopied(false), 1500);
@@ -56096,14 +56293,14 @@ var LeftSidebar = memo40(
56096
56293
  onAddAssetToTimeline
56097
56294
  }, ref) {
56098
56295
  const [tab, setTab] = useState96(getPersistedTab);
56099
- const tabRef = useRef108(tab);
56296
+ const tabRef = useRef109(tab);
56100
56297
  tabRef.current = tab;
56101
- const selectTab = useCallback117((t) => {
56298
+ const selectTab = useCallback118((t) => {
56102
56299
  setTab(t);
56103
56300
  localStorage.setItem(STORAGE_KEY, t);
56104
56301
  trackStudioEvent("tab_switch", { panel: "left_sidebar", tab: t });
56105
56302
  }, []);
56106
- const getTab = useCallback117(() => tabRef.current, []);
56303
+ const getTab = useCallback118(() => tabRef.current, []);
56107
56304
  useImperativeHandle(ref, () => ({ selectTab, getTab }), [selectTab, getTab]);
56108
56305
  return /* @__PURE__ */ jsx126(
56109
56306
  "div",
@@ -56444,7 +56641,7 @@ function StudioLeftSidebar({
56444
56641
  handleImportFiles,
56445
56642
  handleContentChange
56446
56643
  } = useFileManagerContext();
56447
- const handleRenderComposition = useCallback118(
56644
+ const handleRenderComposition = useCallback119(
56448
56645
  async (comp) => {
56449
56646
  await waitForPendingDomEditSaves();
56450
56647
  const { format, quality, fps } = getPersistedRenderSettings();
@@ -56564,13 +56761,13 @@ function StudioLeftSidebar({
56564
56761
  }
56565
56762
 
56566
56763
  // src/components/StudioRightPanel.tsx
56567
- import { useCallback as useCallback134, useEffect as useEffect89, useMemo as useMemo45, useRef as useRef114 } from "react";
56764
+ import { useCallback as useCallback135, useEffect as useEffect90, useRef as useRef115 } from "react";
56568
56765
 
56569
56766
  // src/components/editor/LayersPanel.tsx
56570
- import { memo as memo41, useState as useState99, useCallback as useCallback120, useEffect as useEffect84, useRef as useRef110 } from "react";
56767
+ import { memo as memo41, useState as useState99, useCallback as useCallback121, useEffect as useEffect84, useRef as useRef111 } from "react";
56571
56768
 
56572
56769
  // src/components/editor/useLayerDrag.ts
56573
- import { useRef as useRef109, useState as useState98, useCallback as useCallback119 } from "react";
56770
+ import { useRef as useRef110, useState as useState98, useCallback as useCallback120 } from "react";
56574
56771
  var DRAG_THRESHOLD_PX3 = 4;
56575
56772
  function isLayerDraggable(layer) {
56576
56773
  if (!(layer.selector || layer.id)) return false;
@@ -56637,10 +56834,10 @@ function useLayerDrag({
56637
56834
  onReorder,
56638
56835
  onSingleSibling
56639
56836
  }) {
56640
- const dragRef = useRef109(null);
56837
+ const dragRef = useRef110(null);
56641
56838
  const [dragKey, setDragKey] = useState98(null);
56642
56839
  const [insertionLineY, setInsertionLineY] = useState98(null);
56643
- const handleRowPointerDown = useCallback119(
56840
+ const handleRowPointerDown = useCallback120(
56644
56841
  (layerIndex, e) => {
56645
56842
  if (e.button !== 0) return;
56646
56843
  const layer = visibleLayers[layerIndex];
@@ -56667,7 +56864,7 @@ function useLayerDrag({
56667
56864
  },
56668
56865
  [visibleLayers, scrollContainerRef, onSingleSibling]
56669
56866
  );
56670
- const handleContainerPointerMove = useCallback119(
56867
+ const handleContainerPointerMove = useCallback120(
56671
56868
  (e) => {
56672
56869
  const drag = dragRef.current;
56673
56870
  if (!drag) return;
@@ -56693,7 +56890,7 @@ function useLayerDrag({
56693
56890
  },
56694
56891
  [visibleLayers, scrollContainerRef]
56695
56892
  );
56696
- const handleContainerPointerUp = useCallback119(() => {
56893
+ const handleContainerPointerUp = useCallback120(() => {
56697
56894
  const drag = dragRef.current;
56698
56895
  dragRef.current = null;
56699
56896
  setDragKey(null);
@@ -56842,15 +57039,15 @@ var LayersPanel = memo41(function LayersPanel2() {
56842
57039
  } = useDomEditContext();
56843
57040
  const [layers, setLayers] = useState99([]);
56844
57041
  const [collapsed, setCollapsed] = useState99({});
56845
- const prevDocVersionRef = useRef110(0);
56846
- const scrollContainerRef = useRef110(null);
57042
+ const prevDocVersionRef = useRef111(0);
57043
+ const scrollContainerRef = useRef111(null);
56847
57044
  const mirrorLayerReorderToTimeline = useLayerReorderTimelineMirror();
56848
57045
  const { scheduleReveal } = useLayerRevealOverride({
56849
57046
  isPlaying,
56850
57047
  selectedElement: domEditSelection?.element ?? null
56851
57048
  });
56852
57049
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
56853
- const collectLayers = useCallback120(() => {
57050
+ const collectLayers = useCallback121(() => {
56854
57051
  const iframe = previewIframeRef.current;
56855
57052
  if (!iframe) return;
56856
57053
  let doc = null;
@@ -56897,7 +57094,7 @@ var LayersPanel = memo41(function LayersPanel2() {
56897
57094
  throttle.cancel();
56898
57095
  };
56899
57096
  }, [collectLayers]);
56900
- const resolveSelection = useCallback120(
57097
+ const resolveSelection = useCallback121(
56901
57098
  (layer) => {
56902
57099
  let el = layer.element;
56903
57100
  if (!el.isConnected) {
@@ -56917,7 +57114,7 @@ var LayersPanel = memo41(function LayersPanel2() {
56917
57114
  },
56918
57115
  [activeCompPath, isMasterView, previewIframeRef, activeGroupElement]
56919
57116
  );
56920
- const seekToLayer = useCallback120(
57117
+ const seekToLayer = useCallback121(
56921
57118
  async (layer) => {
56922
57119
  const selection = await resolveSelection(layer);
56923
57120
  if (!selection) return;
@@ -56946,7 +57143,7 @@ var LayersPanel = memo41(function LayersPanel2() {
56946
57143
  },
56947
57144
  [currentTime, resolveSelection, timelineElements]
56948
57145
  );
56949
- const handleSelectLayer = useCallback120(
57146
+ const handleSelectLayer = useCallback121(
56950
57147
  async (layer) => {
56951
57148
  const selection = await resolveSelection(layer);
56952
57149
  if (!selection) return;
@@ -56956,7 +57153,7 @@ var LayersPanel = memo41(function LayersPanel2() {
56956
57153
  },
56957
57154
  [resolveSelection, applyDomSelection, seekToLayer, scheduleReveal]
56958
57155
  );
56959
- const handleLayerDoubleClick = useCallback120(
57156
+ const handleLayerDoubleClick = useCallback121(
56960
57157
  async (layer) => {
56961
57158
  const selection = await resolveSelection(layer);
56962
57159
  if (selection?.element.hasAttribute("data-hf-group")) {
@@ -56967,7 +57164,7 @@ var LayersPanel = memo41(function LayersPanel2() {
56967
57164
  },
56968
57165
  [resolveSelection, setActiveGroupElement, handleSelectLayer]
56969
57166
  );
56970
- const handleLayerHover = useCallback120(
57167
+ const handleLayerHover = useCallback121(
56971
57168
  async (layer) => {
56972
57169
  if (!layer) {
56973
57170
  updateDomEditHoverSelection(null);
@@ -56978,11 +57175,11 @@ var LayersPanel = memo41(function LayersPanel2() {
56978
57175
  },
56979
57176
  [resolveSelection, updateDomEditHoverSelection]
56980
57177
  );
56981
- const toggleCollapse = useCallback120((key, e) => {
57178
+ const toggleCollapse = useCallback121((key, e) => {
56982
57179
  e.stopPropagation();
56983
57180
  setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }));
56984
57181
  }, []);
56985
- const handleReorder = useCallback120(
57182
+ const handleReorder = useCallback121(
56986
57183
  (event) => {
56987
57184
  const { siblingLayers, fromIndex, toIndex } = event;
56988
57185
  const reordered = [...siblingLayers];
@@ -57045,7 +57242,7 @@ var LayersPanel = memo41(function LayersPanel2() {
57045
57242
  );
57046
57243
  const selectedKey = domEditSelection ? getDomEditLayerKey(domEditSelection) : null;
57047
57244
  const visibleLayers = getVisibleLayers(layers, collapsed);
57048
- const handleSingleSibling = useCallback120(() => {
57245
+ const handleSingleSibling = useCallback121(() => {
57049
57246
  showToast("Only one layer at this level", "info");
57050
57247
  }, [showToast]);
57051
57248
  const {
@@ -57174,10 +57371,10 @@ var LayersPanel = memo41(function LayersPanel2() {
57174
57371
  });
57175
57372
 
57176
57373
  // src/captions/components/CaptionPropertyPanel.tsx
57177
- import { memo as memo43, useCallback as useCallback122, useState as useState100 } from "react";
57374
+ import { memo as memo43, useCallback as useCallback123, useState as useState100 } from "react";
57178
57375
 
57179
57376
  // src/captions/components/CaptionAnimationPanel.tsx
57180
- import { memo as memo42, useCallback as useCallback121 } from "react";
57377
+ import { memo as memo42, useCallback as useCallback122 } from "react";
57181
57378
 
57182
57379
  // src/captions/components/shared.tsx
57183
57380
  import { jsx as jsx130, jsxs as jsxs113 } from "react/jsx-runtime";
@@ -57340,25 +57537,25 @@ var CaptionAnimationPanel = memo42(function CaptionAnimationPanel2() {
57340
57537
  }
57341
57538
  const group = resolvedGroupId ? model?.groups.get(resolvedGroupId) : void 0;
57342
57539
  const animation = group?.animation;
57343
- const handleEntranceChange = useCallback121(
57540
+ const handleEntranceChange = useCallback122(
57344
57541
  (update) => {
57345
57542
  if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "entrance", update);
57346
57543
  },
57347
57544
  [resolvedGroupId, updateGroupAnimation]
57348
57545
  );
57349
- const handleHighlightChange = useCallback121(
57546
+ const handleHighlightChange = useCallback122(
57350
57547
  (update) => {
57351
57548
  if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "highlight", update);
57352
57549
  },
57353
57550
  [resolvedGroupId, updateGroupAnimation]
57354
57551
  );
57355
- const handleExitChange = useCallback121(
57552
+ const handleExitChange = useCallback122(
57356
57553
  (update) => {
57357
57554
  if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "exit", update);
57358
57555
  },
57359
57556
  [resolvedGroupId, updateGroupAnimation]
57360
57557
  );
57361
- const handleApplyToAll = useCallback121(() => {
57558
+ const handleApplyToAll = useCallback122(() => {
57362
57559
  if (animation) applyAnimationToAll(animation);
57363
57560
  }, [animation, applyAnimationToAll]);
57364
57561
  if (!group || !resolvedGroupId || !animation) {
@@ -57436,7 +57633,7 @@ var CaptionPropertyPanel = memo43(function CaptionPropertyPanel2({
57436
57633
  ...groupStyle,
57437
57634
  ...segmentOverrides
57438
57635
  };
57439
- const applyToIframeDom = useCallback122(
57636
+ const applyToIframeDom = useCallback123(
57440
57637
  (updates) => {
57441
57638
  const iframe = iframeRef.current;
57442
57639
  if (!iframe || !model) return;
@@ -57510,7 +57707,7 @@ var CaptionPropertyPanel = memo43(function CaptionPropertyPanel2({
57510
57707
  },
57511
57708
  [iframeRef, model, selectedSegmentIds]
57512
57709
  );
57513
- const handleStyleChange = useCallback122(
57710
+ const handleStyleChange = useCallback123(
57514
57711
  (updates) => {
57515
57712
  if (selectedGroupId) {
57516
57713
  updateGroupStyle(selectedGroupId, updates);
@@ -57610,7 +57807,7 @@ var CaptionPropertyPanel = memo43(function CaptionPropertyPanel2({
57610
57807
  });
57611
57808
 
57612
57809
  // src/components/editor/BlockParamsPanel.tsx
57613
- import { memo as memo44, useState as useState101, useCallback as useCallback123 } from "react";
57810
+ import { memo as memo44, useState as useState101, useCallback as useCallback124 } from "react";
57614
57811
  import { jsx as jsx133, jsxs as jsxs116 } from "react/jsx-runtime";
57615
57812
  var BlockParamsPanel = memo44(function BlockParamsPanel2({
57616
57813
  blockTitle,
@@ -57625,7 +57822,7 @@ var BlockParamsPanel = memo44(function BlockParamsPanel2({
57625
57822
  }
57626
57823
  return initial;
57627
57824
  });
57628
- const handleChange = useCallback123((key, value) => {
57825
+ const handleChange = useCallback124((key, value) => {
57629
57826
  setValues((prev) => ({ ...prev, [key]: value }));
57630
57827
  }, []);
57631
57828
  return /* @__PURE__ */ jsxs116("div", { className: "flex flex-col h-full", children: [
@@ -57735,10 +57932,10 @@ function ParamControl({
57735
57932
  }
57736
57933
 
57737
57934
  // src/components/renders/RenderQueue.tsx
57738
- import { memo as memo46, useState as useState103, useRef as useRef111, useEffect as useEffect85, useId as useId2 } from "react";
57935
+ import { memo as memo46, useState as useState103, useRef as useRef112, useEffect as useEffect85, useId as useId2 } from "react";
57739
57936
 
57740
57937
  // src/components/renders/RenderQueueItem.tsx
57741
- import { memo as memo45, useCallback as useCallback124, useState as useState102 } from "react";
57938
+ import { memo as memo45, useCallback as useCallback125, useState as useState102 } from "react";
57742
57939
  import { Fragment as Fragment42, jsx as jsx134, jsxs as jsxs117 } from "react/jsx-runtime";
57743
57940
  function formatDuration2(ms) {
57744
57941
  if (ms < 1e3) return `${ms}ms`;
@@ -57761,10 +57958,10 @@ var RenderQueueItem = memo45(function RenderQueueItem2({
57761
57958
  const [videoReady, setVideoReady] = useState102(false);
57762
57959
  const [confirmingDelete, setConfirmingDelete] = useState102(false);
57763
57960
  const fileSrc = `/api/projects/${projectId}/renders/file/${job.filename}`;
57764
- const handleOpen = useCallback124(() => {
57961
+ const handleOpen = useCallback125(() => {
57765
57962
  window.open(fileSrc, "_blank");
57766
57963
  }, [fileSrc]);
57767
- const handleDownload = useCallback124(
57964
+ const handleDownload = useCallback125(
57768
57965
  (e) => {
57769
57966
  e.stopPropagation();
57770
57967
  const a = document.createElement("a");
@@ -58025,7 +58222,7 @@ var FORMAT_INFO = {
58025
58222
  };
58026
58223
  function FormatInfoTooltip({ format }) {
58027
58224
  const [open, setOpen] = useState103(false);
58028
- const timeoutRef = useRef111(void 0);
58225
+ const timeoutRef = useRef112(void 0);
58029
58226
  const panelId = useId2();
58030
58227
  const show = () => {
58031
58228
  clearTimeout(timeoutRef.current);
@@ -58239,7 +58436,7 @@ var RenderQueue = memo46(function RenderQueue2({
58239
58436
  onDismissActionError,
58240
58437
  compositionDimensions
58241
58438
  }) {
58242
- const listRef = useRef111(null);
58439
+ const listRef = useRef112(null);
58243
58440
  useEffect85(() => {
58244
58441
  if (listRef.current) {
58245
58442
  listRef.current.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
@@ -58348,11 +58545,11 @@ var RenderQueue = memo46(function RenderQueue2({
58348
58545
  });
58349
58546
 
58350
58547
  // src/components/panels/SlideshowPanel.tsx
58351
- import { useState as useState105, useEffect as useEffect86, useCallback as useCallback126, useRef as useRef112 } from "react";
58548
+ import { useState as useState105, useEffect as useEffect86, useCallback as useCallback127, useRef as useRef113 } from "react";
58352
58549
  import { parseSlideshowManifest } from "@hyperframes/core/slideshow";
58353
58550
 
58354
58551
  // src/components/panels/SlideshowSubPanels.tsx
58355
- import { useState as useState104, useCallback as useCallback125, useId as useId3 } from "react";
58552
+ import { useState as useState104, useCallback as useCallback126, useId as useId3 } from "react";
58356
58553
  import { jsx as jsx136, jsxs as jsxs119 } from "react/jsx-runtime";
58357
58554
  function SectionHeader({
58358
58555
  children,
@@ -58537,7 +58734,7 @@ function BranchTree({
58537
58734
  }) {
58538
58735
  const [newLabel, setNewLabel] = useState104("");
58539
58736
  const inputId = useId3();
58540
- const handleCreate = useCallback125(() => {
58737
+ const handleCreate = useCallback126(() => {
58541
58738
  const label = newLabel.trim();
58542
58739
  if (!label) return;
58543
58740
  onCreateSequence(label);
@@ -58598,7 +58795,7 @@ function BranchItem({
58598
58795
  }) {
58599
58796
  const [editing, setEditing] = useState104(false);
58600
58797
  const [draft, setDraft] = useState104(seq.label);
58601
- const commitRename = useCallback125(() => {
58798
+ const commitRename = useCallback126(() => {
58602
58799
  const label = draft.trim();
58603
58800
  if (label && label !== seq.label) onRename(seq.id, label);
58604
58801
  setEditing(false);
@@ -58696,7 +58893,7 @@ function HotspotTool({
58696
58893
  const selectedElementId = domEditSelection?.element?.id ?? null;
58697
58894
  const selectedHfId = domEditSelection?.hfId ?? null;
58698
58895
  const elementKey2 = selectedElementId || selectedHfId;
58699
- const handleMakeHotspot = useCallback125(() => {
58896
+ const handleMakeHotspot = useCallback126(() => {
58700
58897
  if (!selectedSceneId || !targetSequenceId || !elementKey2) return;
58701
58898
  const id = `hotspot-${elementKey2}-${generateId()}`;
58702
58899
  const label = hotspotLabel.trim() || elementKey2;
@@ -58985,8 +59182,8 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
58985
59182
  );
58986
59183
  const currentTime = usePlayerStore((s) => s.currentTime);
58987
59184
  const { domEditSelection } = useDomEditSelectionContext();
58988
- const manifestRef = useRef112(manifest);
58989
- const notesCtrlRef = useRef112(makeSlideshowNotesController());
59185
+ const manifestRef = useRef113(manifest);
59186
+ const notesCtrlRef = useRef113(makeSlideshowNotesController());
58990
59187
  useEffect86(() => {
58991
59188
  if (!compHtml) {
58992
59189
  notesCtrlRef.current.flush();
@@ -59000,7 +59197,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59000
59197
  manifestRef.current = parsed;
59001
59198
  setSelectedSequenceId(null);
59002
59199
  }, [compHtml]);
59003
- const applyManifest = useCallback126(
59200
+ const applyManifest = useCallback127(
59004
59201
  async (next) => {
59005
59202
  const merged = notesCtrlRef.current.mergeIntoDiscrete(next);
59006
59203
  setManifest(merged);
@@ -59013,7 +59210,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59013
59210
  },
59014
59211
  [onPersist]
59015
59212
  );
59016
- const applyNotesManifest = useCallback126(
59213
+ const applyNotesManifest = useCallback127(
59017
59214
  (next) => {
59018
59215
  setManifest(next);
59019
59216
  manifestRef.current = next;
@@ -59027,7 +59224,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59027
59224
  ctrl.flush();
59028
59225
  };
59029
59226
  }, []);
59030
- const toggleSection = useCallback126((key) => {
59227
+ const toggleSection = useCallback127((key) => {
59031
59228
  setExpandedSections((prev) => {
59032
59229
  const next = new Set(prev);
59033
59230
  if (next.has(key)) {
@@ -59041,25 +59238,25 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59041
59238
  const activeSlides = selectedSequenceId ? (manifest.slideSequences ?? []).find((s) => s.id === selectedSequenceId)?.slides ?? [] : manifest.slides;
59042
59239
  const selectedSlide = activeSlides.find((s) => s.sceneId === selectedSceneId);
59043
59240
  const sequences = manifest.slideSequences ?? [];
59044
- const handleSelectBranchSlide = useCallback126((sequenceId, sceneId) => {
59241
+ const handleSelectBranchSlide = useCallback127((sequenceId, sceneId) => {
59045
59242
  setSelectedSceneId(sceneId);
59046
59243
  setSelectedSequenceId(sequenceId);
59047
59244
  }, []);
59048
- const handleToggleSlide = useCallback126(
59245
+ const handleToggleSlide = useCallback127(
59049
59246
  (sceneId) => {
59050
59247
  applyManifest(toggleMainLineSlide(manifestRef.current, sceneId)).catch(() => {
59051
59248
  });
59052
59249
  },
59053
59250
  [applyManifest]
59054
59251
  );
59055
- const handleReorder = useCallback126(
59252
+ const handleReorder = useCallback127(
59056
59253
  (sceneId, dir) => {
59057
59254
  applyManifest(reorderMainLineSlide(manifestRef.current, sceneId, dir)).catch(() => {
59058
59255
  });
59059
59256
  },
59060
59257
  [applyManifest]
59061
59258
  );
59062
- const handleSetNotes = useCallback126(
59259
+ const handleSetNotes = useCallback127(
59063
59260
  (notes) => {
59064
59261
  if (!selectedSceneId) return;
59065
59262
  applyNotesManifest(
@@ -59068,7 +59265,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59068
59265
  },
59069
59266
  [selectedSceneId, selectedSequenceId, applyNotesManifest]
59070
59267
  );
59071
- const handleMarkFragment = useCallback126(() => {
59268
+ const handleMarkFragment = useCallback127(() => {
59072
59269
  if (!selectedSceneId) return;
59073
59270
  applyManifest(
59074
59271
  addFragment(
@@ -59080,7 +59277,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59080
59277
  ).catch(() => {
59081
59278
  });
59082
59279
  }, [selectedSceneId, selectedSequenceId, currentTime, applyManifest]);
59083
- const handleRemoveFragment = useCallback126(
59280
+ const handleRemoveFragment = useCallback127(
59084
59281
  (time) => {
59085
59282
  if (!selectedSceneId) return;
59086
59283
  applyManifest(
@@ -59090,7 +59287,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59090
59287
  },
59091
59288
  [selectedSceneId, selectedSequenceId, applyManifest]
59092
59289
  );
59093
- const handleCreateSequence = useCallback126(
59290
+ const handleCreateSequence = useCallback127(
59094
59291
  (label) => {
59095
59292
  const id = `seq-${generateId()}`;
59096
59293
  applyManifest(createSequence(manifestRef.current, id, label)).catch(() => {
@@ -59098,14 +59295,14 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59098
59295
  },
59099
59296
  [applyManifest]
59100
59297
  );
59101
- const handleRenameSequence = useCallback126(
59298
+ const handleRenameSequence = useCallback127(
59102
59299
  (id, label) => {
59103
59300
  applyManifest(renameSequence(manifestRef.current, id, label)).catch(() => {
59104
59301
  });
59105
59302
  },
59106
59303
  [applyManifest]
59107
59304
  );
59108
- const handleDeleteSequence = useCallback126(
59305
+ const handleDeleteSequence = useCallback127(
59109
59306
  (id) => {
59110
59307
  const seq = (manifestRef.current.slideSequences ?? []).find((s) => s.id === id);
59111
59308
  const count = seq?.slides.length ?? 0;
@@ -59119,7 +59316,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59119
59316
  },
59120
59317
  [applyManifest]
59121
59318
  );
59122
- const handleAssign = useCallback126(
59319
+ const handleAssign = useCallback127(
59123
59320
  (sequenceId, sceneId, assign) => {
59124
59321
  applyManifest(assignToBranch(manifestRef.current, sequenceId, sceneId, assign)).catch(
59125
59322
  () => {
@@ -59128,7 +59325,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59128
59325
  },
59129
59326
  [applyManifest]
59130
59327
  );
59131
- const handleAddHotspot = useCallback126(
59328
+ const handleAddHotspot = useCallback127(
59132
59329
  (sceneId, hotspot) => {
59133
59330
  applyManifest(
59134
59331
  addHotspot(manifestRef.current, sceneId, hotspot, selectedSequenceId ?? void 0)
@@ -59137,7 +59334,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59137
59334
  },
59138
59335
  [selectedSequenceId, applyManifest]
59139
59336
  );
59140
- const handleRemoveHotspot = useCallback126(
59337
+ const handleRemoveHotspot = useCallback127(
59141
59338
  (sceneId, hotspotId) => {
59142
59339
  applyManifest(
59143
59340
  removeHotspot(manifestRef.current, sceneId, hotspotId, selectedSequenceId ?? void 0)
@@ -59241,10 +59438,10 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59241
59438
  }
59242
59439
 
59243
59440
  // src/components/panels/VariablesPanel.tsx
59244
- import { memo as memo47, useCallback as useCallback130, useEffect as useEffect88, useMemo as useMemo44, useState as useState109 } from "react";
59441
+ import { memo as memo47, useCallback as useCallback131, useEffect as useEffect88, useMemo as useMemo45, useState as useState109 } from "react";
59245
59442
 
59246
59443
  // src/hooks/useVariablesPersist.ts
59247
- import { useCallback as useCallback127 } from "react";
59444
+ import { useCallback as useCallback128 } from "react";
59248
59445
  function useVariablesPersist({
59249
59446
  sdkSession,
59250
59447
  activeCompPath,
@@ -59255,7 +59452,7 @@ function useVariablesPersist({
59255
59452
  domEditSaveTimestampRef,
59256
59453
  publishSdkSession
59257
59454
  }) {
59258
- return useCallback127(
59455
+ return useCallback128(
59259
59456
  async (label, mutate) => {
59260
59457
  if (!sdkSession) return false;
59261
59458
  const path = activeCompPath ?? "index.html";
@@ -59292,10 +59489,10 @@ function useVariablesPersist({
59292
59489
  }
59293
59490
 
59294
59491
  // src/components/panels/VariablesOtherCompositions.tsx
59295
- import { useCallback as useCallback129, useState as useState108 } from "react";
59492
+ import { useCallback as useCallback130, useState as useState108 } from "react";
59296
59493
 
59297
59494
  // src/hooks/useProjectCompositionVariables.ts
59298
- import { useCallback as useCallback128, useEffect as useEffect87, useState as useState106 } from "react";
59495
+ import { useCallback as useCallback129, useEffect as useEffect87, useState as useState106 } from "react";
59299
59496
  import { openComposition as openComposition4 } from "@hyperframes/sdk";
59300
59497
  async function readGroup(path, readProjectFile) {
59301
59498
  let content;
@@ -59338,7 +59535,7 @@ function useProjectCompositionVariables(fileTree, excludePath, readProjectFile,
59338
59535
  }
59339
59536
  function useEditVariablesInFile(deps) {
59340
59537
  const { readProjectFile, writeProjectFile, recordEdit, reloadPreview, domEditSaveTimestampRef } = deps;
59341
- return useCallback128(
59538
+ return useCallback129(
59342
59539
  async (path, label, mutate) => {
59343
59540
  const originalContent = await readProjectFile(path);
59344
59541
  await persistSdkSerialize(
@@ -59718,7 +59915,7 @@ function VariablesOtherCompositions({
59718
59915
  domEditSaveTimestampRef
59719
59916
  });
59720
59917
  const [editingKey, setEditingKey] = useState108(null);
59721
- const onSave = useCallback129(
59918
+ const onSave = useCallback130(
59722
59919
  (path, decl) => {
59723
59920
  setEditingKey(null);
59724
59921
  void editInFile(
@@ -59729,7 +59926,7 @@ function VariablesOtherCompositions({
59729
59926
  },
59730
59927
  [editInFile]
59731
59928
  );
59732
- const onRemove = useCallback129(
59929
+ const onRemove = useCallback130(
59733
59930
  (path, id) => {
59734
59931
  void editInFile(
59735
59932
  path,
@@ -59945,27 +60142,27 @@ var VariablesPanel = memo47(function VariablesPanel2({
59945
60142
  domEditSaveTimestampRef,
59946
60143
  publishSdkSession
59947
60144
  });
59948
- const declarations = useMemo44(
60145
+ const declarations = useMemo45(
59949
60146
  () => sdkSession?.getVariableDeclarations() ?? [],
59950
60147
  // eslint-disable-next-line react-hooks/exhaustive-deps
59951
60148
  [sdkSession, refreshKey, revision]
59952
60149
  );
59953
- const usage = useMemo44(
60150
+ const usage = useMemo45(
59954
60151
  () => sdkSession?.getVariableUsage() ?? null,
59955
60152
  // eslint-disable-next-line react-hooks/exhaustive-deps
59956
60153
  [sdkSession, refreshKey, revision]
59957
60154
  );
59958
- const issues = useMemo44(
60155
+ const issues = useMemo45(
59959
60156
  () => previewValues && sdkSession ? sdkSession.validateVariableValues(previewValues) : [],
59960
60157
  // eslint-disable-next-line react-hooks/exhaustive-deps
59961
60158
  [sdkSession, previewValues, refreshKey, revision]
59962
60159
  );
59963
- const effectiveValues = useMemo44(
60160
+ const effectiveValues = useMemo45(
59964
60161
  () => sdkSession?.getVariableValues(previewValues ?? void 0) ?? {},
59965
60162
  // eslint-disable-next-line react-hooks/exhaustive-deps
59966
60163
  [sdkSession, previewValues, refreshKey, revision]
59967
60164
  );
59968
- const copyToClipboard = useCallback130(
60165
+ const copyToClipboard = useCallback131(
59969
60166
  (text, what) => {
59970
60167
  void copyTextToClipboard(text).then(
59971
60168
  (ok) => showToast(
@@ -59976,7 +60173,7 @@ var VariablesPanel = memo47(function VariablesPanel2({
59976
60173
  },
59977
60174
  [showToast]
59978
60175
  );
59979
- const dropPreviewOverride = useCallback130(
60176
+ const dropPreviewOverride = useCallback131(
59980
60177
  (id) => {
59981
60178
  if (previewValues && id in previewValues) {
59982
60179
  const next = { ...previewValues };
@@ -59986,7 +60183,7 @@ var VariablesPanel = memo47(function VariablesPanel2({
59986
60183
  },
59987
60184
  [previewValues, setPreviewValues]
59988
60185
  );
59989
- const commitPreviewValue = useCallback130(
60186
+ const commitPreviewValue = useCallback131(
59990
60187
  (id, value, declDefault) => {
59991
60188
  const next = { ...previewValues ?? {} };
59992
60189
  if (JSON.stringify(value) === JSON.stringify(declDefault)) {
@@ -59999,7 +60196,7 @@ var VariablesPanel = memo47(function VariablesPanel2({
59999
60196
  },
60000
60197
  [previewValues, setPreviewValues, reloadPreview]
60001
60198
  );
60002
- const runSchemaEdit = useCallback130(
60199
+ const runSchemaEdit = useCallback131(
60003
60200
  async (label, mutate) => {
60004
60201
  try {
60005
60202
  const changed = await persistVariables(label, mutate);
@@ -60013,7 +60210,7 @@ var VariablesPanel = memo47(function VariablesPanel2({
60013
60210
  },
60014
60211
  [persistVariables, showToast]
60015
60212
  );
60016
- const handleAdd = useCallback130(
60213
+ const handleAdd = useCallback131(
60017
60214
  (decl) => {
60018
60215
  if (!sdkSession) return;
60019
60216
  const check = sdkSession.can({ type: "declareVariable", declaration: decl });
@@ -60026,7 +60223,7 @@ var VariablesPanel = memo47(function VariablesPanel2({
60026
60223
  },
60027
60224
  [sdkSession, runSchemaEdit, showToast]
60028
60225
  );
60029
- const handleUpdate = useCallback130(
60226
+ const handleUpdate = useCallback131(
60030
60227
  (decl) => {
60031
60228
  if (!sdkSession) return;
60032
60229
  const check = sdkSession.can({
@@ -60046,7 +60243,7 @@ var VariablesPanel = memo47(function VariablesPanel2({
60046
60243
  },
60047
60244
  [sdkSession, runSchemaEdit, showToast]
60048
60245
  );
60049
- const handleRemove = useCallback130(
60246
+ const handleRemove = useCallback131(
60050
60247
  (id) => {
60051
60248
  if (!sdkSession) return;
60052
60249
  const check = sdkSession.can({ type: "removeVariableDeclaration", id });
@@ -60062,18 +60259,18 @@ var VariablesPanel = memo47(function VariablesPanel2({
60062
60259
  },
60063
60260
  [sdkSession, runSchemaEdit, dropPreviewOverride, showToast]
60064
60261
  );
60065
- const handleSetDefault = useCallback130(
60262
+ const handleSetDefault = useCallback131(
60066
60263
  (id, value) => {
60067
60264
  void runSchemaEdit(`Set default for "${id}"`, (s) => s.setVariableValue(id, value));
60068
60265
  dropPreviewOverride(id);
60069
60266
  },
60070
60267
  [runSchemaEdit, dropPreviewOverride]
60071
60268
  );
60072
- const resetPreview = useCallback130(() => {
60269
+ const resetPreview = useCallback131(() => {
60073
60270
  setPreviewValues(null);
60074
60271
  reloadPreview();
60075
60272
  }, [setPreviewValues, reloadPreview]);
60076
- const handleBind = useCallback130(
60273
+ const handleBind = useCallback131(
60077
60274
  // Guard chain (session, selection, type-compat) — one branch per guard.
60078
60275
  // fallow-ignore-next-line complexity
60079
60276
  (action, id) => {
@@ -60206,7 +60403,7 @@ function PanelTabButton({
60206
60403
  }
60207
60404
 
60208
60405
  // src/hooks/useSlideshowPersist.ts
60209
- import { useCallback as useCallback131 } from "react";
60406
+ import { useCallback as useCallback132 } from "react";
60210
60407
 
60211
60408
  // src/utils/setSlideshowManifest.ts
60212
60409
  import {
@@ -60264,7 +60461,7 @@ function useSlideshowPersist({
60264
60461
  publishSdkSession,
60265
60462
  coalesceKey
60266
60463
  }) {
60267
- return useCallback131(
60464
+ return useCallback132(
60268
60465
  async (manifest) => {
60269
60466
  if (!sdkSession) return;
60270
60467
  const path = activeCompPath ?? "index.html";
@@ -60298,26 +60495,65 @@ function useSlideshowPersist({
60298
60495
  );
60299
60496
  }
60300
60497
 
60498
+ // src/hooks/useSlideshowTabState.ts
60499
+ import { useEffect as useEffect89, useMemo as useMemo46 } from "react";
60500
+ import { SLIDESHOW_ISLAND_TYPE as SLIDESHOW_ISLAND_TYPE2, slideshowIslandRegex as slideshowIslandRegex2 } from "@hyperframes/core/slideshow";
60501
+ function useSlideshowTabState(params) {
60502
+ const { editingFileContent, previewIframeRef, refreshKey, rightPanelTab, setRightPanelTab } = params;
60503
+ const isSlideshowComposition = useMemo46(() => {
60504
+ if (!editingFileContent || !editingFileContent.includes(SLIDESHOW_ISLAND_TYPE2)) return false;
60505
+ return slideshowIslandRegex2("i").test(editingFileContent);
60506
+ }, [editingFileContent]);
60507
+ const slideshowScenes = useMemo46(() => {
60508
+ try {
60509
+ const win = previewIframeRef.current?.contentWindow;
60510
+ return (win?.__clipManifest?.scenes ?? []).map((s) => ({
60511
+ id: s.id,
60512
+ label: s.label,
60513
+ start: s.start,
60514
+ duration: s.duration
60515
+ }));
60516
+ } catch {
60517
+ return [];
60518
+ }
60519
+ }, [previewIframeRef, rightPanelTab, refreshKey]);
60520
+ useEffect89(() => {
60521
+ if (rightPanelTab === "slideshow" && !isSlideshowComposition) {
60522
+ setRightPanelTab("renders");
60523
+ }
60524
+ }, [rightPanelTab, isSlideshowComposition, setRightPanelTab]);
60525
+ return { isSlideshowComposition, slideshowScenes };
60526
+ }
60527
+
60301
60528
  // src/components/DesignPanelPromoteProvider.tsx
60302
- import { useCallback as useCallback132 } from "react";
60529
+ import { useCallback as useCallback133 } from "react";
60303
60530
  import { jsx as jsx143 } from "react/jsx-runtime";
60304
60531
  function DesignPanelPromoteProvider({
60305
60532
  selection,
60306
60533
  projectId,
60307
60534
  activeCompPath,
60308
60535
  showToast,
60536
+ forceReloadSharedSdkSession,
60309
60537
  children,
60310
60538
  ...persistDeps
60311
60539
  }) {
60312
60540
  const targetPath = selection?.sourceFile || activeCompPath || "index.html";
60313
60541
  const handle = useSdkSession(projectId, targetPath, persistDeps.domEditSaveTimestampRef);
60314
- const persist2 = useVariablesPersist({
60542
+ const rawPersist = useVariablesPersist({
60315
60543
  ...persistDeps,
60316
60544
  sdkSession: handle.session,
60317
60545
  publishSdkSession: handle.publish,
60318
60546
  activeCompPath: targetPath
60319
60547
  });
60320
- const handlePersistError = useCallback132(
60548
+ const persist2 = useCallback133(
60549
+ async (label, mutate) => {
60550
+ const committed = await rawPersist(label, mutate);
60551
+ if (committed) forceReloadSharedSdkSession?.();
60552
+ return committed;
60553
+ },
60554
+ [rawPersist, forceReloadSharedSdkSession]
60555
+ );
60556
+ const handlePersistError = useCallback133(
60321
60557
  (error) => showToast(getStudioSaveErrorMessage(error), "error"),
60322
60558
  [showToast]
60323
60559
  );
@@ -60552,14 +60788,14 @@ async function applyColorGradingScopeUpdate({
60552
60788
  }
60553
60789
 
60554
60790
  // src/hooks/useInspectorSplitResize.ts
60555
- import { useCallback as useCallback133, useRef as useRef113, useState as useState110 } from "react";
60791
+ import { useCallback as useCallback134, useRef as useRef114, useState as useState110 } from "react";
60556
60792
  var MIN_INSPECTOR_SPLIT_PERCENT = 20;
60557
60793
  var MAX_INSPECTOR_SPLIT_PERCENT = 75;
60558
60794
  function useInspectorSplitResize() {
60559
60795
  const [layersPanePercent, setLayersPanePercent] = useState110(40);
60560
- const splitContainerRef = useRef113(null);
60561
- const splitDragRef = useRef113(null);
60562
- const handleInspectorSplitResizeStart = useCallback133(
60796
+ const splitContainerRef = useRef114(null);
60797
+ const splitDragRef = useRef114(null);
60798
+ const handleInspectorSplitResizeStart = useCallback134(
60563
60799
  (event) => {
60564
60800
  event.preventDefault();
60565
60801
  event.currentTarget.setPointerCapture(event.pointerId);
@@ -60572,7 +60808,7 @@ function useInspectorSplitResize() {
60572
60808
  },
60573
60809
  [layersPanePercent]
60574
60810
  );
60575
- const handleInspectorSplitResizeMove = useCallback133((event) => {
60811
+ const handleInspectorSplitResizeMove = useCallback134((event) => {
60576
60812
  const drag = splitDragRef.current;
60577
60813
  if (!drag || drag.height <= 0) return;
60578
60814
  const deltaPercent = (event.clientY - drag.startY) / drag.height * 100;
@@ -60582,7 +60818,7 @@ function useInspectorSplitResize() {
60582
60818
  );
60583
60819
  setLayersPanePercent(next);
60584
60820
  }, []);
60585
- const handleInspectorSplitResizeEnd = useCallback133(() => {
60821
+ const handleInspectorSplitResizeEnd = useCallback134(() => {
60586
60822
  splitDragRef.current = null;
60587
60823
  }, []);
60588
60824
  return {
@@ -60605,6 +60841,7 @@ function StudioRightPanel({
60605
60841
  onToggleRecording,
60606
60842
  sdkSession,
60607
60843
  publishSdkSession,
60844
+ forceReloadSdkSession,
60608
60845
  reloadPreview,
60609
60846
  domEditSaveTimestampRef,
60610
60847
  recordEdit,
@@ -60617,6 +60854,7 @@ function StudioRightPanel({
60617
60854
  setRightPanelTab,
60618
60855
  rightInspectorPanes,
60619
60856
  toggleRightInspectorPane,
60857
+ setExclusiveRightInspectorPane,
60620
60858
  handlePanelResizeStart,
60621
60859
  handlePanelResizeMove,
60622
60860
  handlePanelResizeEnd
@@ -60683,7 +60921,8 @@ function StudioRightPanel({
60683
60921
  refreshFileTree,
60684
60922
  readProjectFile,
60685
60923
  writeProjectFile,
60686
- fileTree
60924
+ fileTree,
60925
+ editingFile
60687
60926
  } = useFileManagerContext();
60688
60927
  const onPersistSlideshow = useSlideshowPersist({
60689
60928
  sdkSession,
@@ -60713,8 +60952,8 @@ function StudioRightPanel({
60713
60952
  handleInspectorSplitResizeMove,
60714
60953
  handleInspectorSplitResizeEnd
60715
60954
  } = useInspectorSplitResize();
60716
- const backgroundRemovalAbortRef = useRef114(null);
60717
- useEffect89(
60955
+ const backgroundRemovalAbortRef = useRef115(null);
60956
+ useEffect90(
60718
60957
  () => () => {
60719
60958
  backgroundRemovalAbortRef.current?.abort();
60720
60959
  },
@@ -60722,19 +60961,13 @@ function StudioRightPanel({
60722
60961
  );
60723
60962
  const renderJobs = renderQueue.jobs;
60724
60963
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
60725
- const slideshowScenes = useMemo45(() => {
60726
- try {
60727
- const win = previewIframeRef.current?.contentWindow;
60728
- return (win?.__clipManifest?.scenes ?? []).map((s) => ({
60729
- id: s.id,
60730
- label: s.label,
60731
- start: s.start,
60732
- duration: s.duration
60733
- }));
60734
- } catch {
60735
- return [];
60736
- }
60737
- }, [previewIframeRef, rightPanelTab, refreshKey]);
60964
+ const { isSlideshowComposition, slideshowScenes } = useSlideshowTabState({
60965
+ editingFileContent: editingFile?.content,
60966
+ previewIframeRef,
60967
+ refreshKey,
60968
+ rightPanelTab,
60969
+ setRightPanelTab
60970
+ });
60738
60971
  const designPaneOpen = inspectorTabActive && rightInspectorPanes.design && designPanelActive;
60739
60972
  const layersPaneOpen = inspectorTabActive && rightInspectorPanes.layers && STUDIO_INSPECTOR_PANELS_ENABLED;
60740
60973
  const handleInspectorPaneButtonClick = (pane) => {
@@ -60742,9 +60975,13 @@ function StudioRightPanel({
60742
60975
  setRightPanelTab(pane);
60743
60976
  return;
60744
60977
  }
60978
+ if (STUDIO_FLAT_INSPECTOR_ENABLED) {
60979
+ setExclusiveRightInspectorPane(pane);
60980
+ return;
60981
+ }
60745
60982
  toggleRightInspectorPane(pane);
60746
60983
  };
60747
- const handleApplyColorGradingScope = useCallback134(
60984
+ const handleApplyColorGradingScope = useCallback135(
60748
60985
  async (scope, value) => applyColorGradingScopeUpdate({
60749
60986
  scope,
60750
60987
  value,
@@ -60779,7 +61016,7 @@ function StudioRightPanel({
60779
61016
  writeProjectFile
60780
61017
  ]
60781
61018
  );
60782
- const handleRemoveBackground = useCallback134(
61019
+ const handleRemoveBackground = useCallback135(
60783
61020
  // fallow-ignore-next-line complexity
60784
61021
  async (inputPath, options) => {
60785
61022
  const response = await fetch(
@@ -60832,6 +61069,7 @@ function StudioRightPanel({
60832
61069
  recordEdit,
60833
61070
  reloadPreview,
60834
61071
  domEditSaveTimestampRef,
61072
+ forceReloadSharedSdkSession: forceReloadSdkSession,
60835
61073
  children: /* @__PURE__ */ jsx144(
60836
61074
  PropertyPanel,
60837
61075
  {
@@ -60988,7 +61226,7 @@ function StudioRightPanel({
60988
61226
  onClick: () => setRightPanelTab("renders")
60989
61227
  }
60990
61228
  ),
60991
- /* @__PURE__ */ jsx144(
61229
+ isSlideshowComposition && /* @__PURE__ */ jsx144(
60992
61230
  PanelTabButton,
60993
61231
  {
60994
61232
  label: "Slideshow",
@@ -61017,7 +61255,7 @@ function StudioRightPanel({
61017
61255
  onClose: onCloseBlockParams ?? (() => {
61018
61256
  })
61019
61257
  }
61020
- ) : rightPanelTab === "slideshow" ? /* @__PURE__ */ jsx144(
61258
+ ) : rightPanelTab === "slideshow" && isSlideshowComposition ? /* @__PURE__ */ jsx144(
61021
61259
  SlideshowPanel,
61022
61260
  {
61023
61261
  scenes: slideshowScenes,
@@ -61033,7 +61271,7 @@ function StudioRightPanel({
61033
61271
  domEditSaveTimestampRef,
61034
61272
  recordEdit
61035
61273
  }
61036
- ) : layersPaneOpen && designPaneOpen ? /* @__PURE__ */ jsxs124("div", { ref: splitContainerRef, className: "flex h-full min-h-0 min-w-0 flex-col", children: [
61274
+ ) : layersPaneOpen && designPaneOpen && !STUDIO_FLAT_INSPECTOR_ENABLED ? /* @__PURE__ */ jsxs124("div", { ref: splitContainerRef, className: "flex h-full min-h-0 min-w-0 flex-col", children: [
61037
61275
  /* @__PURE__ */ jsx144(
61038
61276
  "div",
61039
61277
  {
@@ -61083,11 +61321,11 @@ function StudioRightPanel({
61083
61321
  }
61084
61322
 
61085
61323
  // src/components/TimelineToolbar.tsx
61086
- import { useEffect as useEffect91, useRef as useRef115 } from "react";
61324
+ import { useEffect as useEffect92, useRef as useRef116 } from "react";
61087
61325
  import { Magnet, MagnifyingGlassMinus, MagnifyingGlassPlus } from "@phosphor-icons/react";
61088
61326
 
61089
61327
  // src/hooks/useEnableKeyframes.ts
61090
- import { useCallback as useCallback135 } from "react";
61328
+ import { useCallback as useCallback136 } from "react";
61091
61329
  var enableKeyframesTransactionCounter = 0;
61092
61330
  function animatedProps(anim) {
61093
61331
  if (!anim) return ["x", "y"];
@@ -61304,7 +61542,7 @@ async function applyArcWaypointAtPlayhead(session, sel, arcAnim, t, iframe) {
61304
61542
  );
61305
61543
  }
61306
61544
  function useEnableKeyframes(sessionRef) {
61307
- return useCallback135(async () => {
61545
+ return useCallback136(async () => {
61308
61546
  const session = sessionRef.current;
61309
61547
  if (!session) return;
61310
61548
  const sel = session.domEditSelection;
@@ -61386,7 +61624,7 @@ function useEnableKeyframes(sessionRef) {
61386
61624
  }
61387
61625
 
61388
61626
  // src/hooks/useKeyframeKeyboard.ts
61389
- import { useEffect as useEffect90, useCallback as useCallback136 } from "react";
61627
+ import { useEffect as useEffect91, useCallback as useCallback137 } from "react";
61390
61628
  function isTextInput(el) {
61391
61629
  if (!el) return false;
61392
61630
  const tag = el.tagName;
@@ -61403,7 +61641,7 @@ function useKeyframeKeyboard({
61403
61641
  onToggleExpand,
61404
61642
  onNudgeKeyframe
61405
61643
  }) {
61406
- const handler = useCallback136(
61644
+ const handler = useCallback137(
61407
61645
  (e) => {
61408
61646
  if (!enabled2) return;
61409
61647
  if (isTextInput(document.activeElement)) return;
@@ -61454,7 +61692,7 @@ function useKeyframeKeyboard({
61454
61692
  onNudgeKeyframe
61455
61693
  ]
61456
61694
  );
61457
- useEffect90(() => {
61695
+ useEffect91(() => {
61458
61696
  if (!enabled2) return;
61459
61697
  window.addEventListener("keydown", handler, { capture: true });
61460
61698
  return () => window.removeEventListener("keydown", handler, { capture: true });
@@ -61465,7 +61703,7 @@ function useKeyframeKeyboard({
61465
61703
  import { Fragment as Fragment45, jsx as jsx145, jsxs as jsxs125 } from "react/jsx-runtime";
61466
61704
  function useKeyframeToggle(session) {
61467
61705
  const currentTime = usePlayerStore((s) => s.currentTime);
61468
- const sessionRef = useRef115(session);
61706
+ const sessionRef = useRef116(session);
61469
61707
  sessionRef.current = session;
61470
61708
  const onToggle = useEnableKeyframes(
61471
61709
  sessionRef
@@ -61509,7 +61747,7 @@ function TimelineToolbar({ domEditSession, onSplitElement }) {
61509
61747
  enabled: STUDIO_KEYFRAMES_ENABLED && Boolean(onToggleKeyframe),
61510
61748
  onAddKeyframe: onToggleKeyframe
61511
61749
  });
61512
- useEffect91(() => {
61750
+ useEffect92(() => {
61513
61751
  const onKeyDown = (e) => {
61514
61752
  if (e.key !== "n" && e.key !== "N") return;
61515
61753
  if (e.metaKey || e.ctrlKey || e.altKey) return;
@@ -61767,20 +62005,20 @@ function TimelineToolbar({ domEditSession, onSplitElement }) {
61767
62005
  }
61768
62006
 
61769
62007
  // src/components/storyboard/StoryboardView.tsx
61770
- import { useState as useState117 } from "react";
62008
+ import { useState as useState118 } from "react";
61771
62009
  import { Copy as Copy2, Check as Check2 } from "@phosphor-icons/react";
61772
62010
 
61773
62011
  // src/hooks/useStoryboard.ts
61774
- import { useCallback as useCallback137, useEffect as useEffect92, useRef as useRef116, useState as useState111 } from "react";
62012
+ import { useCallback as useCallback138, useEffect as useEffect93, useRef as useRef117, useState as useState111 } from "react";
61775
62013
  function useStoryboard(projectId) {
61776
62014
  const [data, setData] = useState111(null);
61777
62015
  const [loading, setLoading] = useState111(true);
61778
62016
  const [error, setError] = useState111(null);
61779
62017
  const [reloadKey, setReloadKey] = useState111(0);
61780
- const hasDataRef = useRef116(false);
61781
- const lastProjectRef = useRef116(null);
61782
- const reload = useCallback137(() => setReloadKey((k) => k + 1), []);
61783
- useEffect92(() => {
62018
+ const hasDataRef = useRef117(false);
62019
+ const lastProjectRef = useRef117(null);
62020
+ const reload = useCallback138(() => setReloadKey((k) => k + 1), []);
62021
+ useEffect93(() => {
61784
62022
  if (!projectId) return;
61785
62023
  let cancelled = false;
61786
62024
  if (lastProjectRef.current !== projectId) {
@@ -61812,7 +62050,7 @@ function useStoryboard(projectId) {
61812
62050
  }
61813
62051
 
61814
62052
  // src/hooks/useProjectSignaturePoll.ts
61815
- import { useEffect as useEffect93, useRef as useRef117 } from "react";
62053
+ import { useEffect as useEffect94, useRef as useRef118 } from "react";
61816
62054
  var POLL_INTERVAL_MS = 2e3;
61817
62055
  async function fetchProjectSignature(projectId) {
61818
62056
  try {
@@ -61825,11 +62063,11 @@ async function fetchProjectSignature(projectId) {
61825
62063
  }
61826
62064
  }
61827
62065
  function useProjectSignaturePoll(projectId, currentSignature, onChange) {
61828
- const signatureRef = useRef117(currentSignature);
61829
- const onChangeRef = useRef117(onChange);
62066
+ const signatureRef = useRef118(currentSignature);
62067
+ const onChangeRef = useRef118(onChange);
61830
62068
  signatureRef.current = currentSignature;
61831
62069
  onChangeRef.current = onChange;
61832
- useEffect93(() => {
62070
+ useEffect94(() => {
61833
62071
  if (!projectId) return;
61834
62072
  let disposed = false;
61835
62073
  let inFlight = false;
@@ -61856,7 +62094,7 @@ function useProjectSignaturePoll(projectId, currentSignature, onChange) {
61856
62094
  }
61857
62095
 
61858
62096
  // src/components/storyboard/StoryboardLoaded.tsx
61859
- import { useEffect as useEffect98, useMemo as useMemo48, useState as useState116 } from "react";
62097
+ import { useEffect as useEffect100, useMemo as useMemo49, useState as useState117 } from "react";
61860
62098
 
61861
62099
  // src/components/storyboard/StoryboardDirection.tsx
61862
62100
  import { jsx as jsx146, jsxs as jsxs126 } from "react/jsx-runtime";
@@ -61879,7 +62117,7 @@ function StoryboardDirection({ globals, frameCount }) {
61879
62117
  }
61880
62118
 
61881
62119
  // src/components/storyboard/FramePoster.tsx
61882
- import { useEffect as useEffect94, useState as useState112 } from "react";
62120
+ import { useEffect as useEffect95, useState as useState112 } from "react";
61883
62121
  import { jsx as jsx147 } from "react/jsx-runtime";
61884
62122
  function FramePoster({
61885
62123
  projectId,
@@ -61890,7 +62128,7 @@ function FramePoster({
61890
62128
  posterVersion
61891
62129
  }) {
61892
62130
  const [failed2, setFailed] = useState112(false);
61893
- useEffect94(() => setFailed(false), [src, seconds, posterVersion]);
62131
+ useEffect95(() => setFailed(false), [src, seconds, posterVersion]);
61894
62132
  if (failed2) {
61895
62133
  return /* @__PURE__ */ jsx147("div", { className: "flex h-full w-full items-center justify-center text-[11px] text-neutral-600", children: "Preview unavailable" });
61896
62134
  }
@@ -62075,41 +62313,24 @@ function StoryboardGrid({
62075
62313
  )) });
62076
62314
  }
62077
62315
 
62078
- // src/components/storyboard/StoryboardStatusLegend.tsx
62079
- import { jsx as jsx150, jsxs as jsxs128 } from "react/jsx-runtime";
62080
- function StoryboardStatusLegend() {
62081
- return /* @__PURE__ */ jsxs128("div", { className: "flex flex-wrap items-center gap-x-5 gap-y-1.5 text-[11px] text-neutral-500", children: [
62082
- /* @__PURE__ */ jsx150("span", { className: "uppercase tracking-wider text-neutral-600", children: "Status" }),
62083
- FRAME_STATUS_ORDER.map((status, i) => /* @__PURE__ */ jsxs128("span", { className: "flex items-center gap-1.5", children: [
62084
- i > 0 && /* @__PURE__ */ jsx150("span", { className: "text-neutral-700", children: "\u2192" }),
62085
- /* @__PURE__ */ jsx150("span", { className: `h-2 w-2 rounded-full ${FRAME_STATUS_META[status].dotClass}` }),
62086
- /* @__PURE__ */ jsx150("span", { className: "font-medium text-neutral-300", children: FRAME_STATUS_META[status].label }),
62087
- /* @__PURE__ */ jsxs128("span", { className: "text-neutral-500", children: [
62088
- "\u2014 ",
62089
- FRAME_STATUS_META[status].description
62090
- ] })
62091
- ] }, status))
62092
- ] });
62093
- }
62094
-
62095
62316
  // src/components/storyboard/StoryboardScriptPanel.tsx
62096
- import { jsx as jsx151, jsxs as jsxs129 } from "react/jsx-runtime";
62317
+ import { jsx as jsx150, jsxs as jsxs128 } from "react/jsx-runtime";
62097
62318
  function StoryboardScriptPanel({ script }) {
62098
62319
  if (!script.exists) return null;
62099
- return /* @__PURE__ */ jsxs129("details", { className: "mt-10 rounded-lg border border-neutral-800 bg-neutral-900/50", children: [
62100
- /* @__PURE__ */ jsxs129("summary", { className: "cursor-pointer select-none px-4 py-3 text-sm font-medium text-neutral-300", children: [
62320
+ return /* @__PURE__ */ jsxs128("details", { className: "mt-10 rounded-lg border border-neutral-800 bg-neutral-900/50", children: [
62321
+ /* @__PURE__ */ jsxs128("summary", { className: "cursor-pointer select-none px-4 py-3 text-sm font-medium text-neutral-300", children: [
62101
62322
  "Narration script",
62102
- /* @__PURE__ */ jsx151("span", { className: "ml-2 font-normal text-neutral-500", children: script.path })
62323
+ /* @__PURE__ */ jsx150("span", { className: "ml-2 font-normal text-neutral-500", children: script.path })
62103
62324
  ] }),
62104
- /* @__PURE__ */ jsx151("pre", { className: "max-h-[420px] overflow-auto whitespace-pre-wrap border-t border-neutral-800 px-4 py-3 text-xs leading-relaxed text-neutral-400", children: script.content })
62325
+ /* @__PURE__ */ jsx150("pre", { className: "max-h-[420px] overflow-auto whitespace-pre-wrap border-t border-neutral-800 px-4 py-3 text-xs leading-relaxed text-neutral-400", children: script.content })
62105
62326
  ] });
62106
62327
  }
62107
62328
 
62108
62329
  // src/components/storyboard/StoryboardSourceEditor.tsx
62109
- import { useCallback as useCallback138, useEffect as useEffect95, useMemo as useMemo46, useRef as useRef118, useState as useState113 } from "react";
62330
+ import { useCallback as useCallback139, useEffect as useEffect96, useMemo as useMemo47, useRef as useRef119, useState as useState113 } from "react";
62110
62331
  import { marked } from "marked";
62111
62332
  import DOMPurify from "dompurify";
62112
- import { jsx as jsx152, jsxs as jsxs130 } from "react/jsx-runtime";
62333
+ import { jsx as jsx151, jsxs as jsxs129 } from "react/jsx-runtime";
62113
62334
  var DISCARD_PROMPT = "Discard unsaved markdown changes?";
62114
62335
  function useEditableFile(path, onSaved) {
62115
62336
  const { readProjectFile, writeProjectFile } = useFileManagerContext();
@@ -62118,7 +62339,7 @@ function useEditableFile(path, onSaved) {
62118
62339
  const [loading, setLoading] = useState113(true);
62119
62340
  const [saving, setSaving] = useState113(false);
62120
62341
  const [error, setError] = useState113(null);
62121
- useEffect95(() => {
62342
+ useEffect96(() => {
62122
62343
  if (!path) return;
62123
62344
  let cancelled = false;
62124
62345
  setLoading(true);
@@ -62136,7 +62357,7 @@ function useEditableFile(path, onSaved) {
62136
62357
  cancelled = true;
62137
62358
  };
62138
62359
  }, [path, readProjectFile]);
62139
- const save = useCallback138(() => {
62360
+ const save = useCallback139(() => {
62140
62361
  if (saving) return;
62141
62362
  setSaving(true);
62142
62363
  setError(null);
@@ -62155,8 +62376,8 @@ function hardenLinks(node) {
62155
62376
  }
62156
62377
  function useMarkdownPreview(source) {
62157
62378
  const [debounced, setDebounced] = useState113(source);
62158
- const primed = useRef118(false);
62159
- useEffect95(() => {
62379
+ const primed = useRef119(false);
62380
+ useEffect96(() => {
62160
62381
  if (!primed.current && source !== "") {
62161
62382
  primed.current = true;
62162
62383
  setDebounced(source);
@@ -62165,7 +62386,7 @@ function useMarkdownPreview(source) {
62165
62386
  const id = window.setTimeout(() => setDebounced(source), 200);
62166
62387
  return () => window.clearTimeout(id);
62167
62388
  }, [source]);
62168
- return useMemo46(() => {
62389
+ return useMemo47(() => {
62169
62390
  const html2 = marked.parse(debounced, { async: false });
62170
62391
  DOMPurify.addHook("afterSanitizeAttributes", hardenLinks);
62171
62392
  try {
@@ -62188,8 +62409,8 @@ function StoryboardSourceEditor({
62188
62409
  const activePath = files.some((f) => f.path === selected) ? selected : files[0]?.path ?? "";
62189
62410
  const file = useEditableFile(activePath, onSaved);
62190
62411
  const previewHtml = useMarkdownPreview(file.content);
62191
- useEffect95(() => onDirtyChange?.(file.dirty), [onDirtyChange, file.dirty]);
62192
- useEffect95(() => {
62412
+ useEffect96(() => onDirtyChange?.(file.dirty), [onDirtyChange, file.dirty]);
62413
+ useEffect96(() => {
62193
62414
  if (!file.dirty) return;
62194
62415
  const onBeforeUnload = (event) => {
62195
62416
  event.preventDefault();
@@ -62203,7 +62424,7 @@ function StoryboardSourceEditor({
62203
62424
  if (file.dirty && !window.confirm(DISCARD_PROMPT)) return;
62204
62425
  setSelected(path);
62205
62426
  };
62206
- return /* @__PURE__ */ jsxs130(
62427
+ return /* @__PURE__ */ jsxs129(
62207
62428
  "div",
62208
62429
  {
62209
62430
  className: "flex flex-1 min-h-0 flex-col",
@@ -62213,8 +62434,8 @@ function StoryboardSourceEditor({
62213
62434
  if (file.dirty && !file.saving) file.save();
62214
62435
  },
62215
62436
  children: [
62216
- /* @__PURE__ */ jsxs130("div", { className: "flex items-center gap-1 border-b border-neutral-800 px-4 py-2", children: [
62217
- files.map((f) => /* @__PURE__ */ jsx152(
62437
+ /* @__PURE__ */ jsxs129("div", { className: "flex items-center gap-1 border-b border-neutral-800 px-4 py-2", children: [
62438
+ files.map((f) => /* @__PURE__ */ jsx151(
62218
62439
  "button",
62219
62440
  {
62220
62441
  type: "button",
@@ -62224,10 +62445,10 @@ function StoryboardSourceEditor({
62224
62445
  },
62225
62446
  f.path
62226
62447
  )),
62227
- /* @__PURE__ */ jsxs130("div", { className: "ml-auto flex items-center gap-3", children: [
62228
- file.error && /* @__PURE__ */ jsx152("span", { className: "text-xs text-red-400", children: file.error }),
62229
- /* @__PURE__ */ jsx152("span", { className: "text-xs text-neutral-500", children: file.saving ? "Saving\u2026" : file.dirty ? "Unsaved changes" : "Saved" }),
62230
- /* @__PURE__ */ jsx152(
62448
+ /* @__PURE__ */ jsxs129("div", { className: "ml-auto flex items-center gap-3", children: [
62449
+ file.error && /* @__PURE__ */ jsx151("span", { className: "text-xs text-red-400", children: file.error }),
62450
+ /* @__PURE__ */ jsx151("span", { className: "text-xs text-neutral-500", children: file.saving ? "Saving\u2026" : file.dirty ? "Unsaved changes" : "Saved" }),
62451
+ /* @__PURE__ */ jsx151(
62231
62452
  "button",
62232
62453
  {
62233
62454
  type: "button",
@@ -62239,12 +62460,12 @@ function StoryboardSourceEditor({
62239
62460
  )
62240
62461
  ] })
62241
62462
  ] }),
62242
- /* @__PURE__ */ jsxs130("div", { className: "flex flex-1 min-h-0", children: [
62243
- /* @__PURE__ */ jsx152("div", { className: "w-1/2 min-w-0 border-r border-neutral-800", children: file.loading ? /* @__PURE__ */ jsxs130("div", { className: "p-4 text-sm text-neutral-500", children: [
62463
+ /* @__PURE__ */ jsxs129("div", { className: "flex flex-1 min-h-0", children: [
62464
+ /* @__PURE__ */ jsx151("div", { className: "w-1/2 min-w-0 border-r border-neutral-800", children: file.loading ? /* @__PURE__ */ jsxs129("div", { className: "p-4 text-sm text-neutral-500", children: [
62244
62465
  "Loading ",
62245
62466
  activePath,
62246
62467
  "\u2026"
62247
- ] }) : /* @__PURE__ */ jsx152(
62468
+ ] }) : /* @__PURE__ */ jsx151(
62248
62469
  SourceEditor,
62249
62470
  {
62250
62471
  content: file.content,
@@ -62253,7 +62474,7 @@ function StoryboardSourceEditor({
62253
62474
  onChange: file.setContent
62254
62475
  }
62255
62476
  ) }),
62256
- /* @__PURE__ */ jsx152("div", { className: "w-1/2 min-w-0 overflow-auto bg-neutral-950 px-6 py-4", children: /* @__PURE__ */ jsx152("div", { className: PREVIEW_PROSE, dangerouslySetInnerHTML: { __html: previewHtml } }) })
62477
+ /* @__PURE__ */ jsx151("div", { className: "w-1/2 min-w-0 overflow-auto bg-neutral-950 px-6 py-4", children: /* @__PURE__ */ jsx151("div", { className: PREVIEW_PROSE, dangerouslySetInnerHTML: { __html: previewHtml } }) })
62257
62478
  ] })
62258
62479
  ]
62259
62480
  }
@@ -62261,9 +62482,38 @@ function StoryboardSourceEditor({
62261
62482
  }
62262
62483
 
62263
62484
  // src/components/storyboard/StoryboardFrameFocus.tsx
62264
- import { useCallback as useCallback139, useEffect as useEffect96, useState as useState114 } from "react";
62265
- import { setFrameStatus, setFrameVoiceover } from "@hyperframes/core/storyboard";
62266
- import { jsx as jsx153, jsxs as jsxs131 } from "react/jsx-runtime";
62485
+ import { useCallback as useCallback140, useEffect as useEffect98, useState as useState115 } from "react";
62486
+ import { setFrameVoiceover } from "@hyperframes/core/storyboard";
62487
+
62488
+ // src/components/storyboard/AgentChatMessageButton.tsx
62489
+ import { useEffect as useEffect97, useState as useState114 } from "react";
62490
+ import { jsx as jsx152 } from "react/jsx-runtime";
62491
+ var APPLY_STORYBOARD_FEEDBACK_MESSAGE = "Read the storyboard feedback I saved in .hyperframes/frame-comments.json and revise the frames.";
62492
+ function AgentChatMessageButton({
62493
+ message,
62494
+ label = "Copy prompt for agent",
62495
+ onCopied
62496
+ }) {
62497
+ const [copyState, setCopyState] = useState114("idle");
62498
+ useEffect97(() => {
62499
+ if (copyState !== "copied") return;
62500
+ const timeout = window.setTimeout(() => setCopyState("again"), 3e3);
62501
+ return () => window.clearTimeout(timeout);
62502
+ }, [copyState]);
62503
+ const copyMessage = async () => {
62504
+ try {
62505
+ await navigator.clipboard.writeText(message);
62506
+ setCopyState("copied");
62507
+ onCopied?.();
62508
+ } catch {
62509
+ setCopyState("failed");
62510
+ }
62511
+ };
62512
+ return /* @__PURE__ */ jsx152(Button, { size: "sm", variant: "secondary", onClick: () => void copyMessage(), children: copyState === "copied" ? "Copied \u2014 paste in your agent chat" : copyState === "again" ? "Copy again" : copyState === "failed" ? "Copy failed" : label });
62513
+ }
62514
+
62515
+ // src/components/storyboard/StoryboardFrameFocus.tsx
62516
+ import { jsx as jsx153, jsxs as jsxs130 } from "react/jsx-runtime";
62267
62517
  function StoryboardFrameFocus({
62268
62518
  projectId,
62269
62519
  storyboardPath,
@@ -62273,24 +62523,38 @@ function StoryboardFrameFocus({
62273
62523
  onNavigate,
62274
62524
  onSaved,
62275
62525
  onSelectComposition,
62526
+ scriptExists,
62527
+ commentDraft,
62528
+ onCommentDraftChange,
62529
+ pendingComment,
62530
+ pendingCommentCount,
62531
+ commentDraftCount,
62532
+ commentsSubmitState,
62533
+ commentsSubmitError,
62534
+ feedbackMessageCopied,
62535
+ onFeedbackMessageCopied,
62536
+ onSaveFeedback,
62276
62537
  posterVersion
62277
62538
  }) {
62278
62539
  const { readProjectFile, writeProjectFile } = useFileManagerContext();
62279
- const { setViewMode } = useViewMode();
62280
- const [draft, setDraft] = useState114(frame.voiceover ?? "");
62281
- const [busy, setBusy] = useState114(false);
62282
- const [error, setError] = useState114(null);
62283
- const applyEdit = useCallback139(
62540
+ const { setViewMode, registerViewModeGuard } = useViewMode();
62541
+ const [draft, setDraft] = useState115(frame.voiceover ?? "");
62542
+ const [savedVoiceover, setSavedVoiceover] = useState115(frame.voiceover ?? "");
62543
+ const [busy, setBusy] = useState115(false);
62544
+ const [error, setError] = useState115(null);
62545
+ const applyEdit = useCallback140(
62284
62546
  async (edit) => {
62285
- if (busy) return;
62547
+ if (busy) return false;
62286
62548
  setBusy(true);
62287
62549
  setError(null);
62288
62550
  try {
62289
62551
  const source = await readProjectFile(storyboardPath);
62290
62552
  await writeProjectFile(storyboardPath, edit(source));
62291
62553
  onSaved();
62554
+ return true;
62292
62555
  } catch (err) {
62293
62556
  setError(err instanceof Error ? err.message : "failed to save");
62557
+ return false;
62294
62558
  } finally {
62295
62559
  setBusy(false);
62296
62560
  }
@@ -62298,12 +62562,13 @@ function StoryboardFrameFocus({
62298
62562
  [readProjectFile, writeProjectFile, storyboardPath, onSaved, busy]
62299
62563
  );
62300
62564
  const title = frame.title ?? `Frame ${frame.index}`;
62301
- const dirty = draft !== (frame.voiceover ?? "");
62302
- const canOpenPreview = frame.srcExists && Boolean(frame.src);
62303
- const saveVoiceover = useCallback139(() => {
62304
- return applyEdit((src) => setFrameVoiceover(src, frame.index, draft));
62565
+ const dirty = draft !== savedVoiceover;
62566
+ const canOpenPreview = frame.status !== "outline" && frame.srcExists && Boolean(frame.src);
62567
+ const saveVoiceover = useCallback140(async () => {
62568
+ const saved = await applyEdit((src) => setFrameVoiceover(src, frame.index, draft));
62569
+ if (saved) setSavedVoiceover(draft);
62305
62570
  }, [applyEdit, frame.index, draft]);
62306
- useEffect96(() => {
62571
+ useEffect98(() => {
62307
62572
  if (!dirty) return;
62308
62573
  const onBeforeUnload = (e) => {
62309
62574
  e.preventDefault();
@@ -62311,14 +62576,18 @@ function StoryboardFrameFocus({
62311
62576
  window.addEventListener("beforeunload", onBeforeUnload);
62312
62577
  return () => window.removeEventListener("beforeunload", onBeforeUnload);
62313
62578
  }, [dirty]);
62314
- const confirmLeave = () => !dirty || window.confirm("Discard unsaved voiceover changes?");
62579
+ const confirmLeave = useCallback140(
62580
+ () => !dirty || window.confirm("Discard unsaved voiceover changes?"),
62581
+ [dirty]
62582
+ );
62583
+ useEffect98(() => registerViewModeGuard(confirmLeave), [confirmLeave, registerViewModeGuard]);
62315
62584
  const handleBack = () => {
62316
62585
  if (confirmLeave()) onBack();
62317
62586
  };
62318
62587
  const handleNavigate = (delta) => {
62319
62588
  if (confirmLeave()) onNavigate(delta);
62320
62589
  };
62321
- useEffect96(() => {
62590
+ useEffect98(() => {
62322
62591
  const onKey = (e) => {
62323
62592
  const el = document.activeElement;
62324
62593
  if (el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement) return;
@@ -62330,11 +62599,11 @@ function StoryboardFrameFocus({
62330
62599
  return () => window.removeEventListener("keydown", onKey);
62331
62600
  });
62332
62601
  const openInPreview = () => {
62602
+ if (!setViewMode("timeline")) return;
62333
62603
  if (frame.src) onSelectComposition(frame.src);
62334
- setViewMode("timeline");
62335
62604
  };
62336
- return /* @__PURE__ */ jsxs131("div", { className: "flex flex-1 min-h-0 flex-col bg-neutral-950 text-neutral-200", children: [
62337
- /* @__PURE__ */ jsxs131("div", { className: "flex items-center gap-3 border-b border-neutral-800 px-4 py-2", children: [
62605
+ return /* @__PURE__ */ jsxs130("div", { className: "flex w-full max-w-[100vw] flex-1 min-h-0 min-w-0 flex-col overflow-hidden bg-neutral-950 text-neutral-200", children: [
62606
+ /* @__PURE__ */ jsxs130("div", { className: "flex items-center gap-3 border-b border-neutral-800 px-4 py-2", children: [
62338
62607
  /* @__PURE__ */ jsx153(
62339
62608
  "button",
62340
62609
  {
@@ -62344,13 +62613,13 @@ function StoryboardFrameFocus({
62344
62613
  children: "\u2190 Board"
62345
62614
  }
62346
62615
  ),
62347
- /* @__PURE__ */ jsxs131("span", { className: "text-sm font-medium text-neutral-200", children: [
62616
+ /* @__PURE__ */ jsxs130("span", { className: "min-w-0 flex-1 truncate text-sm font-medium text-neutral-200", children: [
62348
62617
  "Frame ",
62349
62618
  frame.number ?? frame.index,
62350
62619
  " \u2014 ",
62351
62620
  title
62352
62621
  ] }),
62353
- /* @__PURE__ */ jsxs131("div", { className: "ml-auto flex items-center gap-1", children: [
62622
+ /* @__PURE__ */ jsxs130("div", { className: "flex shrink-0 items-center gap-1", children: [
62354
62623
  /* @__PURE__ */ jsx153(
62355
62624
  NavButton,
62356
62625
  {
@@ -62369,8 +62638,35 @@ function StoryboardFrameFocus({
62369
62638
  )
62370
62639
  ] })
62371
62640
  ] }),
62372
- /* @__PURE__ */ jsxs131("div", { className: "flex flex-1 min-h-0", children: [
62373
- /* @__PURE__ */ jsx153("div", { className: "flex w-3/5 min-w-0 items-center justify-center bg-neutral-900/40 p-8", children: /* @__PURE__ */ jsx153("div", { className: "aspect-video w-full max-w-[900px] overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900", children: canOpenPreview && frame.src ? /* @__PURE__ */ jsx153(
62641
+ (commentDraftCount > 0 || pendingCommentCount > 0) && /* @__PURE__ */ jsxs130("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b border-neutral-800 bg-neutral-900/80 px-4 py-2", children: [
62642
+ /* @__PURE__ */ jsxs130("div", { children: [
62643
+ /* @__PURE__ */ jsx153("div", { className: "text-xs font-medium text-neutral-200", children: commentDraftCount > 0 ? "Feedback ready to save" : "Feedback saved" }),
62644
+ /* @__PURE__ */ jsx153("div", { className: "text-[11px] text-neutral-500", children: commentDraftCount > 0 ? "Save this batch and copy the message for your agent." : feedbackMessageCopied ? "Message copied \u2014 paste it in your terminal or IDE agent chat." : "The agent has not been notified yet." })
62645
+ ] }),
62646
+ commentDraftCount > 0 ? /* @__PURE__ */ jsxs130(
62647
+ Button,
62648
+ {
62649
+ size: "sm",
62650
+ variant: "primary",
62651
+ onClick: onSaveFeedback,
62652
+ loading: commentsSubmitState === "saving",
62653
+ children: [
62654
+ "Save & copy message (",
62655
+ commentDraftCount,
62656
+ ")"
62657
+ ]
62658
+ }
62659
+ ) : /* @__PURE__ */ jsx153(
62660
+ AgentChatMessageButton,
62661
+ {
62662
+ message: APPLY_STORYBOARD_FEEDBACK_MESSAGE,
62663
+ label: feedbackMessageCopied ? "Copy again" : "Copy prompt for agent",
62664
+ onCopied: onFeedbackMessageCopied
62665
+ }
62666
+ )
62667
+ ] }),
62668
+ /* @__PURE__ */ jsxs130("div", { className: "flex flex-1 min-h-0 flex-col overflow-auto lg:flex-row lg:overflow-hidden", children: [
62669
+ /* @__PURE__ */ jsx153("div", { className: "flex w-full shrink-0 items-center justify-center bg-neutral-900/40 p-4 sm:p-8 lg:h-full lg:w-3/5", children: /* @__PURE__ */ jsx153("div", { className: "aspect-video w-full max-w-[900px] overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900", children: canOpenPreview && frame.src ? /* @__PURE__ */ jsx153(
62374
62670
  FramePoster,
62375
62671
  {
62376
62672
  projectId,
@@ -62380,32 +62676,32 @@ function StoryboardFrameFocus({
62380
62676
  fit: "contain",
62381
62677
  posterVersion
62382
62678
  }
62383
- ) : /* @__PURE__ */ jsx153("div", { className: "flex h-full w-full items-center justify-center text-sm text-neutral-600", children: frame.status === "outline" ? "Not built yet" : "No preview" }) }) }),
62384
- /* @__PURE__ */ jsxs131("div", { className: "w-2/5 min-w-0 space-y-6 overflow-auto border-l border-neutral-800 px-6 py-5", children: [
62385
- /* @__PURE__ */ jsx153(
62386
- StatusRow,
62387
- {
62388
- status: frame.status,
62389
- busy,
62390
- onSet: (s) => applyEdit((src) => setFrameStatus(src, frame.index, s))
62391
- }
62392
- ),
62393
- /* @__PURE__ */ jsxs131("div", { className: "flex flex-wrap gap-x-6 gap-y-1 text-[11px] text-neutral-500", children: [
62394
- frame.duration && /* @__PURE__ */ jsxs131("span", { children: [
62679
+ ) : /* @__PURE__ */ jsx153(FramePlan, { frame }) }) }),
62680
+ /* @__PURE__ */ jsxs130("div", { className: "w-full shrink-0 space-y-6 border-t border-neutral-800 px-4 py-5 sm:px-6 lg:h-full lg:w-2/5 lg:overflow-auto lg:border-t-0 lg:border-l", children: [
62681
+ /* @__PURE__ */ jsx153(ReadOnlyStatus, { status: frame.status }),
62682
+ /* @__PURE__ */ jsxs130("div", { className: "flex flex-wrap gap-x-6 gap-y-1 text-[11px] text-neutral-500", children: [
62683
+ frame.duration && /* @__PURE__ */ jsxs130("span", { children: [
62395
62684
  "Duration ",
62396
62685
  frame.duration
62397
62686
  ] }),
62398
- frame.transitionIn && /* @__PURE__ */ jsxs131("span", { children: [
62687
+ frame.transitionIn && /* @__PURE__ */ jsxs130("span", { children: [
62399
62688
  "Transition ",
62400
62689
  frame.transitionIn
62401
62690
  ] })
62402
62691
  ] }),
62403
- /* @__PURE__ */ jsxs131("section", { children: [
62404
- /* @__PURE__ */ jsxs131("div", { className: "mb-1 flex items-center justify-between", children: [
62405
- /* @__PURE__ */ jsxs131("h3", { className: "text-xs font-semibold uppercase tracking-wider text-neutral-400", children: [
62406
- "\u{1F399} Voiceover ",
62407
- /* @__PURE__ */ jsx153("span", { className: "font-normal normal-case text-neutral-600", children: "guide" })
62408
- ] }),
62692
+ /* @__PURE__ */ jsxs130("section", { children: [
62693
+ /* @__PURE__ */ jsxs130("div", { className: "mb-1 flex items-center justify-between", children: [
62694
+ /* @__PURE__ */ jsxs130(
62695
+ "h3",
62696
+ {
62697
+ className: "text-xs font-semibold uppercase tracking-wider text-neutral-400",
62698
+ title: "Storyboard voiceover is a guide; SCRIPT.md is the final TTS source.",
62699
+ children: [
62700
+ "\u{1F399} Voiceover ",
62701
+ /* @__PURE__ */ jsx153("span", { className: "font-normal normal-case text-neutral-600", children: "guide" })
62702
+ ]
62703
+ }
62704
+ ),
62409
62705
  /* @__PURE__ */ jsx153(
62410
62706
  Button,
62411
62707
  {
@@ -62414,8 +62710,7 @@ function StoryboardFrameFocus({
62414
62710
  onClick: saveVoiceover,
62415
62711
  disabled: !dirty,
62416
62712
  loading: busy,
62417
- className: "bg-emerald-600 text-white enabled:hover:bg-emerald-500 shadow-none",
62418
- children: busy ? "Saving\u2026" : "Save"
62713
+ children: busy ? "Saving\u2026" : "Save voiceover"
62419
62714
  }
62420
62715
  )
62421
62716
  ] }),
@@ -62424,22 +62719,53 @@ function StoryboardFrameFocus({
62424
62719
  {
62425
62720
  value: draft,
62426
62721
  onChange: (e) => setDraft(e.target.value),
62427
- onBlur: () => {
62428
- if (dirty && !busy) void saveVoiceover();
62429
- },
62430
62722
  rows: 3,
62431
62723
  placeholder: "What the narrator says over this frame\u2026",
62432
62724
  className: "w-full resize-y rounded border border-neutral-800 bg-neutral-900 p-2 text-sm text-neutral-200 outline-none focus:border-neutral-600"
62433
62725
  }
62434
62726
  ),
62435
- /* @__PURE__ */ jsx153("p", { className: "mt-1 text-[11px] text-neutral-600", children: "A draft guide. SCRIPT.md locks the final narration that drives TTS." }),
62727
+ /* @__PURE__ */ jsx153("p", { className: "mt-1 text-[11px] text-neutral-600", children: dirty ? "Unsaved changes" : scriptExists ? "Saved. SCRIPT.md drives final TTS." : "Saved to STORYBOARD.md. This voiceover guides narration for the frame." }),
62436
62728
  error && /* @__PURE__ */ jsx153("p", { className: "mt-1 text-[11px] text-red-400", children: error })
62437
62729
  ] }),
62438
- frame.narrative && /* @__PURE__ */ jsxs131("section", { children: [
62730
+ /* @__PURE__ */ jsxs130("section", { children: [
62731
+ /* @__PURE__ */ jsx153("div", { className: "mb-1", children: /* @__PURE__ */ jsx153("h3", { className: "text-xs font-semibold uppercase tracking-wider text-neutral-400", children: "Frame feedback" }) }),
62732
+ /* @__PURE__ */ jsx153(
62733
+ "textarea",
62734
+ {
62735
+ value: commentDraft,
62736
+ onChange: (e) => onCommentDraftChange(e.target.value),
62737
+ rows: 3,
62738
+ placeholder: "Tell your agent what to change in this frame\u2026",
62739
+ "aria-label": `Comment on ${title}`,
62740
+ className: "w-full resize-y rounded border border-neutral-800 bg-neutral-900 p-2 text-sm text-neutral-200 placeholder:text-neutral-600 outline-none focus:border-sky-700"
62741
+ }
62742
+ ),
62743
+ pendingCommentCount > 0 ? /* @__PURE__ */ jsxs130("div", { className: "mt-2 flex flex-wrap items-center justify-between gap-2 rounded-md border border-sky-900/70 bg-sky-950/20 px-2.5 py-2", children: [
62744
+ /* @__PURE__ */ jsx153("p", { className: "text-[11px] text-sky-200", children: "Paste the agent prompt in your terminal or IDE chat." }),
62745
+ /* @__PURE__ */ jsx153(
62746
+ AgentChatMessageButton,
62747
+ {
62748
+ message: APPLY_STORYBOARD_FEEDBACK_MESSAGE,
62749
+ onCopied: onFeedbackMessageCopied
62750
+ }
62751
+ )
62752
+ ] }) : /* @__PURE__ */ jsx153("p", { className: "mt-1 text-[11px] text-neutral-600", children: commentDraftCount > 0 ? "Your change is ready. Save it using the review bar above." : "Add a change to prepare feedback for the agent." }),
62753
+ pendingComment && /* @__PURE__ */ jsxs130("p", { className: "mt-1 text-[11px] text-sky-400/90", children: [
62754
+ /* @__PURE__ */ jsx153("span", { className: "font-medium", children: "Pending:" }),
62755
+ " \u201C",
62756
+ pendingComment,
62757
+ "\u201D"
62758
+ ] }),
62759
+ commentsSubmitError && /* @__PURE__ */ jsxs130("p", { className: "mt-1 text-[11px] text-red-400", children: [
62760
+ "Couldn\u2019t submit: ",
62761
+ commentsSubmitError
62762
+ ] })
62763
+ ] }),
62764
+ frame.narrative && /* @__PURE__ */ jsxs130("section", { children: [
62439
62765
  /* @__PURE__ */ jsx153("h3", { className: "mb-1 text-xs font-semibold uppercase tracking-wider text-neutral-400", children: "Narrative" }),
62440
62766
  /* @__PURE__ */ jsx153("p", { className: "whitespace-pre-wrap text-sm text-neutral-300", children: frame.narrative })
62441
62767
  ] }),
62442
- /* @__PURE__ */ jsx153(Button, { size: "sm", variant: "secondary", onClick: openInPreview, disabled: !canOpenPreview, children: "Open in Preview \u2192" })
62768
+ canOpenPreview ? /* @__PURE__ */ jsx153(Button, { size: "sm", variant: "secondary", onClick: openInPreview, children: "Open in Preview \u2192" }) : /* @__PURE__ */ jsx153("div", { className: "rounded-md border border-neutral-800 bg-neutral-900/60 px-3 py-2 text-xs text-neutral-500", children: "Preview becomes available after your agent builds this frame." })
62443
62769
  ] })
62444
62770
  ] })
62445
62771
  ] });
@@ -62460,31 +62786,280 @@ function NavButton({
62460
62786
  }
62461
62787
  );
62462
62788
  }
62463
- function StatusRow({
62464
- status,
62465
- busy,
62466
- onSet
62467
- }) {
62468
- return /* @__PURE__ */ jsxs131("div", { className: "flex items-center gap-2", children: [
62789
+ function ReadOnlyStatus({ status }) {
62790
+ const meta = FRAME_STATUS_META[status];
62791
+ return /* @__PURE__ */ jsxs130("div", { className: "flex items-center gap-2", children: [
62469
62792
  /* @__PURE__ */ jsx153("span", { className: "text-xs font-semibold uppercase tracking-wider text-neutral-500", children: "Status" }),
62470
- /* @__PURE__ */ jsx153("div", { className: "flex items-center gap-0.5 rounded-md bg-neutral-900 p-0.5", children: FRAME_STATUS_ORDER.map((option) => /* @__PURE__ */ jsx153(
62471
- "button",
62793
+ /* @__PURE__ */ jsx153("span", { className: `rounded px-2 py-1 text-xs font-medium ${meta.chipClass}`, children: meta.label }),
62794
+ /* @__PURE__ */ jsx153("span", { className: "text-[11px] text-neutral-600", children: "Updated by your agent" })
62795
+ ] });
62796
+ }
62797
+ function FramePlan({ frame }) {
62798
+ const title = frame.title ?? `Frame ${frame.index}`;
62799
+ const isOutline = frame.status === "outline";
62800
+ return /* @__PURE__ */ jsx153("div", { className: "flex h-full w-full items-center justify-center bg-[radial-gradient(circle_at_center,_rgba(38,38,38,0.8),_rgba(10,10,10,1))] p-10", children: /* @__PURE__ */ jsxs130("div", { className: "max-w-xl text-center", children: [
62801
+ /* @__PURE__ */ jsx153("span", { className: "rounded-full border border-neutral-700 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider text-neutral-400", children: isOutline ? "Planned frame" : "Preview unavailable" }),
62802
+ /* @__PURE__ */ jsx153("h2", { className: "mt-4 text-2xl font-semibold text-neutral-100", children: title }),
62803
+ frame.scene && /* @__PURE__ */ jsx153("p", { className: "mt-3 text-base leading-relaxed text-neutral-300", children: frame.scene }),
62804
+ !frame.scene && frame.narrative && /* @__PURE__ */ jsx153("p", { className: "mt-3 line-clamp-4 text-sm leading-relaxed text-neutral-400", children: frame.narrative }),
62805
+ /* @__PURE__ */ jsx153("p", { className: "mt-5 text-xs text-neutral-600", children: isOutline ? "A visual preview will appear when your agent builds the sketch." : frame.src ? `Frame file not found: ${frame.src}` : "This frame does not link to a source file." })
62806
+ ] }) });
62807
+ }
62808
+
62809
+ // src/components/storyboard/storyboardReviewStage.ts
62810
+ function deriveStoryboardHandoffStep(draftCount, pendingCount) {
62811
+ if (draftCount > 0) return 2;
62812
+ if (pendingCount > 0) return 3;
62813
+ return 1;
62814
+ }
62815
+ function deriveStoryboardReviewStage(frames) {
62816
+ const counts = { outline: 0, built: 0, animated: 0 };
62817
+ for (const frame of frames) counts[frame.status] += 1;
62818
+ let stage;
62819
+ if (frames.length === 0) stage = "empty";
62820
+ else if (counts.outline === frames.length) stage = "plan-review";
62821
+ else if (counts.outline > 0) stage = "sketch-in-progress";
62822
+ else if (counts.built === frames.length) stage = "sketch-review";
62823
+ else if (counts.built > 0) stage = "animation-in-progress";
62824
+ else stage = "final-review";
62825
+ return { stage, counts, frameCount: frames.length };
62826
+ }
62827
+
62828
+ // src/components/storyboard/StoryboardReviewGuide.tsx
62829
+ import { jsx as jsx154, jsxs as jsxs131 } from "react/jsx-runtime";
62830
+ var GUIDE_COPY = {
62831
+ empty: {
62832
+ eyebrow: "Waiting for a plan",
62833
+ title: "The storyboard has no frames yet",
62834
+ body: "Ask your agent to draft the story plan. Frames will appear here automatically."
62835
+ },
62836
+ "plan-review": {
62837
+ eyebrow: "Ready for review",
62838
+ title: "Review the story plan",
62839
+ body: "Check the sequence, scene direction, and voiceover before visual work begins. Leave frame comments, save them, then reply in your terminal or IDE agent chat."
62840
+ },
62841
+ "sketch-in-progress": {
62842
+ eyebrow: "Build in progress",
62843
+ title: "Visual sketches are in progress",
62844
+ body: "New posters appear automatically as your agent builds them. You can comment now; wait until no frames remain in Outline before approving the layouts."
62845
+ },
62846
+ "sketch-review": {
62847
+ eyebrow: "Ready for review",
62848
+ title: "Review the visual direction",
62849
+ body: "Check composition, hierarchy, and copy. Save frame comments, then reply in your terminal or IDE agent chat."
62850
+ },
62851
+ "animation-in-progress": {
62852
+ eyebrow: "Build in progress",
62853
+ title: "Animation is in progress",
62854
+ body: "The board refreshes as frames advance. Review completed frames now; the final review is ready when every frame is Animated."
62855
+ },
62856
+ "final-review": {
62857
+ eyebrow: "Ready for review",
62858
+ title: "Review motion and timing",
62859
+ body: "Every frame is animated. Open a frame in Preview to review it in the timeline, or leave frame comments for another revision."
62860
+ }
62861
+ };
62862
+ var REVIEW_ACTION_COPY = {
62863
+ empty: { body: "Add comments as previews arrive." },
62864
+ "plan-review": {
62865
+ body: "Add comments where you want changes. If everything looks right, approve this pass in agent chat.",
62866
+ approvalMessage: "Approve this storyboard plan and continue to visual sketches."
62867
+ },
62868
+ "sketch-in-progress": {
62869
+ body: "Add comments as previews arrive. You\u2019ll be prompted to approve when this pass is ready."
62870
+ },
62871
+ "sketch-review": {
62872
+ body: "Add comments where you want changes. If everything looks right, approve this pass in agent chat.",
62873
+ approvalMessage: "Approve these storyboard sketches and continue to animation."
62874
+ },
62875
+ "animation-in-progress": {
62876
+ body: "Add comments as previews arrive. You\u2019ll be prompted to approve when this pass is ready."
62877
+ },
62878
+ "final-review": {
62879
+ body: "Add comments where you want changes. If everything looks right, approve this pass in agent chat.",
62880
+ approvalMessage: "Approve this final storyboard review and continue to rendering."
62881
+ }
62882
+ };
62883
+ var REVIEW_STEP_STATES = {
62884
+ [-1]: {
62885
+ textClass: "text-emerald-400",
62886
+ numberClass: "border-emerald-700 bg-emerald-500/10",
62887
+ ariaCurrent: void 0,
62888
+ marker: () => "\u2713"
62889
+ },
62890
+ [0]: {
62891
+ textClass: "text-sky-300",
62892
+ numberClass: "border-sky-500 bg-sky-500/15",
62893
+ ariaCurrent: "step",
62894
+ marker: (number) => number
62895
+ },
62896
+ [1]: {
62897
+ textClass: "text-neutral-600",
62898
+ numberClass: "border-neutral-700",
62899
+ ariaCurrent: void 0,
62900
+ marker: (number) => number
62901
+ }
62902
+ };
62903
+ var REVIEW_STEP_SEPARATOR_CLASS = {
62904
+ 1: "invisible",
62905
+ 2: "",
62906
+ 3: ""
62907
+ };
62908
+ function StoryboardReviewGuide({
62909
+ frames,
62910
+ draftCount,
62911
+ pendingCount,
62912
+ onFeedbackMessageCopied
62913
+ }) {
62914
+ const summary = deriveStoryboardReviewStage(frames);
62915
+ const copy = GUIDE_COPY[summary.stage];
62916
+ const handoffStep = deriveStoryboardHandoffStep(draftCount, pendingCount);
62917
+ const progress = progressLabel(summary);
62918
+ return /* @__PURE__ */ jsxs131("section", { className: "mt-5 rounded-lg border border-neutral-800 bg-neutral-900/60 px-4 py-3", children: [
62919
+ /* @__PURE__ */ jsxs131("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between", children: [
62920
+ /* @__PURE__ */ jsxs131("div", { className: "max-w-3xl", children: [
62921
+ /* @__PURE__ */ jsx154("div", { className: "text-[10px] font-semibold uppercase tracking-wider text-sky-400", children: copy.eyebrow }),
62922
+ /* @__PURE__ */ jsx154("h2", { className: "mt-0.5 text-sm font-semibold text-neutral-100", children: copy.title }),
62923
+ /* @__PURE__ */ jsx154("p", { className: "mt-1 text-xs leading-relaxed text-neutral-400", children: copy.body })
62924
+ ] }),
62925
+ summary.frameCount > 0 && /* @__PURE__ */ jsxs131("div", { className: "shrink-0", children: [
62926
+ /* @__PURE__ */ jsx154("div", { className: "mb-1.5 text-right text-[11px] font-medium text-neutral-300", children: progress }),
62927
+ /* @__PURE__ */ jsx154("div", { className: "flex flex-wrap justify-end gap-1.5", "aria-label": "Frame status summary", children: FRAME_STATUS_ORDER.map((status) => /* @__PURE__ */ jsxs131(
62928
+ "span",
62929
+ {
62930
+ className: `rounded px-2 py-1 text-[10px] font-medium ${FRAME_STATUS_META[status].chipClass}`,
62931
+ children: [
62932
+ summary.counts[status],
62933
+ " ",
62934
+ FRAME_STATUS_META[status].label
62935
+ ]
62936
+ },
62937
+ status
62938
+ )) })
62939
+ ] })
62940
+ ] }),
62941
+ summary.frameCount > 0 && /* @__PURE__ */ jsxs131("div", { className: "mt-3 border-t border-neutral-800 pt-3", children: [
62942
+ /* @__PURE__ */ jsx154(ReviewSteps, { current: handoffStep }),
62943
+ /* @__PURE__ */ jsx154(
62944
+ NextAction,
62945
+ {
62946
+ stage: summary.stage,
62947
+ step: handoffStep,
62948
+ draftCount,
62949
+ onFeedbackMessageCopied
62950
+ }
62951
+ )
62952
+ ] })
62953
+ ] });
62954
+ }
62955
+ function ReviewSteps({ current }) {
62956
+ const steps = ["Review frames", "Save feedback", "Reply in agent chat"];
62957
+ return /* @__PURE__ */ jsx154("ol", { className: "hidden items-center gap-2 sm:flex", "aria-label": "Storyboard review workflow", children: steps.map((label, index) => /* @__PURE__ */ jsx154(
62958
+ ReviewStep,
62959
+ {
62960
+ label,
62961
+ number: index + 1,
62962
+ current
62963
+ },
62964
+ label
62965
+ )) });
62966
+ }
62967
+ function ReviewStep({
62968
+ label,
62969
+ number,
62970
+ current
62971
+ }) {
62972
+ const offset = Math.sign(number - current);
62973
+ const state = REVIEW_STEP_STATES[offset];
62974
+ return /* @__PURE__ */ jsxs131("li", { className: "flex items-center gap-2", children: [
62975
+ /* @__PURE__ */ jsx154(
62976
+ "span",
62472
62977
  {
62473
- type: "button",
62474
- disabled: busy,
62475
- "aria-pressed": status === option,
62476
- title: FRAME_STATUS_META[option].tooltip,
62477
- onClick: () => onSet(option),
62478
- className: `rounded px-2.5 py-1 text-xs font-medium transition-colors disabled:opacity-50 ${status === option ? "bg-neutral-700 text-neutral-100" : "text-neutral-400 hover:text-neutral-200"}`,
62479
- children: FRAME_STATUS_META[option].label
62480
- },
62481
- option
62482
- )) })
62978
+ className: `text-neutral-700 ${REVIEW_STEP_SEPARATOR_CLASS[number]}`,
62979
+ "aria-hidden": "true",
62980
+ children: "\u2192"
62981
+ }
62982
+ ),
62983
+ /* @__PURE__ */ jsxs131(
62984
+ "span",
62985
+ {
62986
+ "aria-current": state.ariaCurrent,
62987
+ className: `flex items-center gap-1.5 text-[11px] font-medium ${state.textClass}`,
62988
+ children: [
62989
+ /* @__PURE__ */ jsx154(
62990
+ "span",
62991
+ {
62992
+ className: `flex h-5 w-5 items-center justify-center rounded-full border text-[10px] ${state.numberClass}`,
62993
+ children: state.marker(number)
62994
+ }
62995
+ ),
62996
+ label
62997
+ ]
62998
+ }
62999
+ )
62483
63000
  ] });
62484
63001
  }
63002
+ function NextAction({
63003
+ stage,
63004
+ step,
63005
+ draftCount,
63006
+ onFeedbackMessageCopied
63007
+ }) {
63008
+ if (step === 3) {
63009
+ return /* @__PURE__ */ jsx154(AgentHandoffAction, { onFeedbackMessageCopied });
63010
+ }
63011
+ if (step === 2) return /* @__PURE__ */ jsx154(SaveFeedbackAction, { draftCount });
63012
+ return /* @__PURE__ */ jsx154(ReviewFramesAction, { stage });
63013
+ }
63014
+ function AgentHandoffAction({ onFeedbackMessageCopied }) {
63015
+ return /* @__PURE__ */ jsxs131("div", { className: "mt-3 flex flex-col gap-3 rounded-md border border-sky-900/70 bg-sky-950/20 px-3 py-2.5 sm:flex-row sm:items-center sm:justify-between", children: [
63016
+ /* @__PURE__ */ jsxs131("div", { className: "max-w-3xl", children: [
63017
+ /* @__PURE__ */ jsx154("div", { className: "text-xs font-semibold text-sky-200", children: "Next: return to your agent chat" }),
63018
+ /* @__PURE__ */ jsx154("p", { className: "mt-0.5 text-[11px] text-neutral-400", children: "Feedback is saved, but the agent has not been notified. Paste this prompt in your terminal or IDE agent chat." })
63019
+ ] }),
63020
+ /* @__PURE__ */ jsx154(
63021
+ AgentChatMessageButton,
63022
+ {
63023
+ message: APPLY_STORYBOARD_FEEDBACK_MESSAGE,
63024
+ onCopied: onFeedbackMessageCopied
63025
+ }
63026
+ )
63027
+ ] });
63028
+ }
63029
+ function SaveFeedbackAction({ draftCount }) {
63030
+ return /* @__PURE__ */ jsx154("div", { className: "mt-3 rounded-md border border-neutral-800 bg-neutral-950/40 px-3 py-2.5", children: /* @__PURE__ */ jsxs131("div", { children: [
63031
+ /* @__PURE__ */ jsx154("div", { className: "text-xs font-semibold text-neutral-200", children: "Next: save your feedback" }),
63032
+ /* @__PURE__ */ jsxs131("p", { className: "mt-0.5 text-[11px] text-neutral-500", children: [
63033
+ draftCount,
63034
+ " frame",
63035
+ draftCount === 1 ? " has" : "s have",
63036
+ " feedback ready. Use Save & copy message above to prepare this batch for your agent."
63037
+ ] })
63038
+ ] }) });
63039
+ }
63040
+ function ReviewFramesAction({ stage }) {
63041
+ const copy = REVIEW_ACTION_COPY[stage];
63042
+ return /* @__PURE__ */ jsxs131("div", { className: "mt-3 flex flex-col gap-3 rounded-md border border-neutral-800 bg-neutral-950/40 px-3 py-2.5 sm:flex-row sm:items-center sm:justify-between", children: [
63043
+ /* @__PURE__ */ jsxs131("div", { children: [
63044
+ /* @__PURE__ */ jsx154("div", { className: "text-xs font-semibold text-neutral-200", children: "Next: review the frames" }),
63045
+ /* @__PURE__ */ jsx154("p", { className: "mt-0.5 text-[11px] text-neutral-500", children: copy.body })
63046
+ ] }),
63047
+ copy.approvalMessage && /* @__PURE__ */ jsx154(AgentChatMessageButton, { message: copy.approvalMessage, label: "Copy approval message" })
63048
+ ] });
63049
+ }
63050
+ function progressLabel(summary) {
63051
+ if (summary.stage === "sketch-in-progress" || summary.stage === "sketch-review") {
63052
+ const ready = summary.frameCount - summary.counts.outline;
63053
+ return `${ready} of ${summary.frameCount} visual sketches ready`;
63054
+ }
63055
+ if (summary.stage === "animation-in-progress" || summary.stage === "final-review") {
63056
+ return `${summary.counts.animated} of ${summary.frameCount} animations ready`;
63057
+ }
63058
+ return `${summary.frameCount} plan frame${summary.frameCount === 1 ? "" : "s"} ready`;
63059
+ }
62485
63060
 
62486
63061
  // src/components/storyboard/useFrameComments.ts
62487
- import { useCallback as useCallback140, useEffect as useEffect97, useMemo as useMemo47, useState as useState115 } from "react";
63062
+ import { useCallback as useCallback141, useEffect as useEffect99, useMemo as useMemo48, useState as useState116 } from "react";
62488
63063
 
62489
63064
  // src/components/storyboard/frameComments.ts
62490
63065
  var FRAME_COMMENTS_PATH = ".hyperframes/frame-comments.json";
@@ -62546,32 +63121,34 @@ function buildCommentsFile(frames, drafts, previous, submittedAt) {
62546
63121
  // src/components/storyboard/useFrameComments.ts
62547
63122
  function useFrameComments(frames) {
62548
63123
  const { writeProjectFile, readOptionalProjectFile } = useFileManagerContext();
62549
- const [drafts, setDrafts] = useState115({});
62550
- const [submitState, setSubmitState] = useState115("idle");
62551
- const [pending, setPending] = useState115(null);
62552
- const refreshPending = useCallback140(async () => {
63124
+ const [drafts, setDrafts] = useState116({});
63125
+ const [submitState, setSubmitState] = useState116("idle");
63126
+ const [submitError, setSubmitError] = useState116(null);
63127
+ const [pending, setPending] = useState116(null);
63128
+ const refreshPending = useCallback141(async () => {
62553
63129
  try {
62554
63130
  const parsed = parseCommentsFile(await readOptionalProjectFile(FRAME_COMMENTS_PATH));
62555
63131
  setPending(parsed && parsed.comments.length > 0 ? parsed.comments : null);
62556
63132
  } catch {
62557
63133
  }
62558
63134
  }, [readOptionalProjectFile]);
62559
- useEffect97(() => {
63135
+ useEffect99(() => {
62560
63136
  void refreshPending();
62561
63137
  const onFocus = () => void refreshPending();
62562
63138
  window.addEventListener("focus", onFocus);
62563
63139
  return () => window.removeEventListener("focus", onFocus);
62564
63140
  }, [refreshPending]);
62565
- const setDraft = useCallback140((index, text) => {
63141
+ const setDraft = useCallback141((index, text) => {
62566
63142
  setDrafts((prev) => ({ ...prev, [index]: text }));
62567
63143
  }, []);
62568
- const draftCount = useMemo47(
63144
+ const draftCount = useMemo48(
62569
63145
  () => Object.values(drafts).filter((text) => text.trim().length > 0).length,
62570
63146
  [drafts]
62571
63147
  );
62572
- const submit = useCallback140(async () => {
62573
- if (draftCount === 0 || submitState === "saving") return;
63148
+ const submit = useCallback141(async () => {
63149
+ if (draftCount === 0 || submitState === "saving") return false;
62574
63150
  setSubmitState("saving");
63151
+ setSubmitError(null);
62575
63152
  try {
62576
63153
  const previous = parseCommentsFile(await readOptionalProjectFile(FRAME_COMMENTS_PATH));
62577
63154
  const file = buildCommentsFile(frames, drafts, previous, (/* @__PURE__ */ new Date()).toISOString());
@@ -62579,16 +63156,28 @@ function useFrameComments(frames) {
62579
63156
  `);
62580
63157
  setDrafts({});
62581
63158
  setPending(file.comments);
62582
- } catch {
63159
+ return true;
63160
+ } catch (err) {
63161
+ setSubmitError(err instanceof Error ? err.message : "Failed to submit comments");
63162
+ return false;
62583
63163
  } finally {
62584
63164
  setSubmitState("idle");
62585
63165
  }
62586
63166
  }, [draftCount, submitState, frames, drafts, readOptionalProjectFile, writeProjectFile]);
62587
- return { drafts, setDraft, draftCount, submitState, submit, pending, refreshPending };
63167
+ return {
63168
+ drafts,
63169
+ setDraft,
63170
+ draftCount,
63171
+ submitState,
63172
+ submitError,
63173
+ submit,
63174
+ pending,
63175
+ refreshPending
63176
+ };
62588
63177
  }
62589
63178
 
62590
63179
  // src/components/storyboard/StoryboardLoaded.tsx
62591
- import { jsx as jsx154, jsxs as jsxs132 } from "react/jsx-runtime";
63180
+ import { Fragment as Fragment46, jsx as jsx155, jsxs as jsxs132 } from "react/jsx-runtime";
62592
63181
  function clampIndex(index, count) {
62593
63182
  return Math.max(1, Math.min(count, index));
62594
63183
  }
@@ -62598,15 +63187,29 @@ function StoryboardLoaded({
62598
63187
  reload,
62599
63188
  onSelectComposition
62600
63189
  }) {
62601
- const [subView, setSubView] = useState116("board");
62602
- const [sourceDirty, setSourceDirty] = useState116(false);
62603
- const [focusedIndex, setFocusedIndex] = useState116(null);
63190
+ const [subView, setSubView] = useState117("board");
63191
+ const [sourceDirty, setSourceDirty] = useState117(false);
63192
+ const [focusedIndex, setFocusedIndex] = useState117(null);
63193
+ const [feedbackMessageCopied, setFeedbackMessageCopied] = useState117(false);
62604
63194
  const comments = useFrameComments(data.frames);
62605
63195
  const { refreshPending } = comments;
62606
- useEffect98(() => {
63196
+ useEffect100(() => {
62607
63197
  void refreshPending();
62608
63198
  }, [data.signature, refreshPending]);
62609
- const sourceFiles = useMemo48(() => {
63199
+ useEffect100(() => {
63200
+ if (comments.draftCount > 0) setFeedbackMessageCopied(false);
63201
+ }, [comments.draftCount]);
63202
+ const saveFeedbackAndCopyMessage = async () => {
63203
+ const saved = await comments.submit();
63204
+ if (!saved) return;
63205
+ try {
63206
+ await navigator.clipboard.writeText(APPLY_STORYBOARD_FEEDBACK_MESSAGE);
63207
+ setFeedbackMessageCopied(true);
63208
+ } catch {
63209
+ setFeedbackMessageCopied(false);
63210
+ }
63211
+ };
63212
+ const sourceFiles = useMemo49(() => {
62610
63213
  const files = [{ path: data.path, label: data.path }];
62611
63214
  if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path });
62612
63215
  return files;
@@ -62620,7 +63223,7 @@ function StoryboardLoaded({
62620
63223
  };
62621
63224
  const focusedFrame = focusedIndex != null ? data.frames.find((f) => f.index === focusedIndex) ?? null : null;
62622
63225
  if (focusedFrame) {
62623
- return /* @__PURE__ */ jsx154(
63226
+ return /* @__PURE__ */ jsx155(
62624
63227
  StoryboardFrameFocus,
62625
63228
  {
62626
63229
  projectId,
@@ -62631,28 +63234,57 @@ function StoryboardLoaded({
62631
63234
  onNavigate: (delta) => setFocusedIndex(clampIndex(focusedFrame.index + delta, data.frames.length)),
62632
63235
  onSaved: reload,
62633
63236
  onSelectComposition,
63237
+ scriptExists: Boolean(data.script?.exists),
63238
+ commentDraft: comments.drafts[focusedFrame.index] ?? "",
63239
+ onCommentDraftChange: (text) => comments.setDraft(focusedFrame.index, text),
63240
+ pendingComment: comments.pending?.find((entry) => entry.frame === focusedFrame.index)?.text ?? null,
63241
+ pendingCommentCount: comments.pending?.length ?? 0,
63242
+ commentDraftCount: comments.draftCount,
63243
+ commentsSubmitState: comments.submitState,
63244
+ commentsSubmitError: comments.submitError,
63245
+ feedbackMessageCopied,
63246
+ onFeedbackMessageCopied: () => setFeedbackMessageCopied(true),
63247
+ onSaveFeedback: () => void saveFeedbackAndCopyMessage(),
62634
63248
  posterVersion: data.signature
62635
63249
  },
62636
63250
  focusedFrame.index
62637
63251
  );
62638
63252
  }
62639
- return /* @__PURE__ */ jsxs132("div", { className: "flex flex-1 min-h-0 flex-col bg-neutral-950 text-neutral-200", children: [
62640
- /* @__PURE__ */ jsxs132("div", { className: "flex items-center gap-3 border-b border-neutral-800 px-4 py-2", children: [
62641
- /* @__PURE__ */ jsx154(SubViewToggle, { value: subView, onChange: changeSubView }),
62642
- subView === "board" && /* @__PURE__ */ jsx154(
63253
+ return /* @__PURE__ */ jsxs132("div", { className: "flex w-full max-w-[100vw] flex-1 min-h-0 min-w-0 flex-col overflow-hidden bg-neutral-950 text-neutral-200", children: [
63254
+ /* @__PURE__ */ jsxs132("div", { className: "flex flex-wrap items-center gap-3 border-b border-neutral-800 px-4 py-2", children: [
63255
+ /* @__PURE__ */ jsx155(SubViewToggle, { value: subView, onChange: changeSubView }),
63256
+ subView === "board" && /* @__PURE__ */ jsx155(
62643
63257
  CommentsSubmitBar,
62644
63258
  {
62645
63259
  draftCount: comments.draftCount,
62646
63260
  pendingCount: comments.pending?.length ?? 0,
62647
63261
  submitState: comments.submitState,
62648
- onSubmit: () => void comments.submit()
63262
+ submitError: comments.submitError,
63263
+ messageCopied: feedbackMessageCopied,
63264
+ onSave: () => void saveFeedbackAndCopyMessage(),
63265
+ onMessageCopied: () => setFeedbackMessageCopied(true)
62649
63266
  }
62650
63267
  )
62651
63268
  ] }),
62652
- subView === "board" ? /* @__PURE__ */ jsx154("div", { className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ jsxs132("div", { className: "mx-auto max-w-[1400px] px-8 py-8", children: [
62653
- /* @__PURE__ */ jsx154(StoryboardDirection, { globals: data.globals, frameCount: data.frames.length }),
62654
- /* @__PURE__ */ jsx154("div", { className: "mt-5", children: /* @__PURE__ */ jsx154(StoryboardStatusLegend, {}) }),
62655
- /* @__PURE__ */ jsx154(
63269
+ subView === "board" ? /* @__PURE__ */ jsx155("div", { className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ jsxs132("div", { className: "mx-auto max-w-[1400px] px-4 py-5 sm:px-8 sm:py-8", children: [
63270
+ /* @__PURE__ */ jsx155(StoryboardDirection, { globals: data.globals, frameCount: data.frames.length }),
63271
+ /* @__PURE__ */ jsx155(
63272
+ StoryboardReviewGuide,
63273
+ {
63274
+ frames: data.frames,
63275
+ draftCount: comments.draftCount,
63276
+ pendingCount: comments.pending?.length ?? 0,
63277
+ onFeedbackMessageCopied: () => setFeedbackMessageCopied(true)
63278
+ }
63279
+ ),
63280
+ /* @__PURE__ */ jsx155(
63281
+ StoryboardWarnings,
63282
+ {
63283
+ warnings: data.warnings,
63284
+ onOpenSource: () => changeSubView("source")
63285
+ }
63286
+ ),
63287
+ /* @__PURE__ */ jsx155(
62656
63288
  StoryboardGrid,
62657
63289
  {
62658
63290
  projectId,
@@ -62664,8 +63296,8 @@ function StoryboardLoaded({
62664
63296
  posterVersion: data.signature
62665
63297
  }
62666
63298
  ),
62667
- data.script && /* @__PURE__ */ jsx154(StoryboardScriptPanel, { script: data.script })
62668
- ] }) }) : /* @__PURE__ */ jsx154(
63299
+ data.script && /* @__PURE__ */ jsx155(StoryboardScriptPanel, { script: data.script })
63300
+ ] }) }) : /* @__PURE__ */ jsx155(
62669
63301
  StoryboardSourceEditor,
62670
63302
  {
62671
63303
  files: sourceFiles,
@@ -62679,24 +63311,67 @@ function CommentsSubmitBar({
62679
63311
  draftCount,
62680
63312
  pendingCount,
62681
63313
  submitState,
62682
- onSubmit
62683
- }) {
62684
- return /* @__PURE__ */ jsxs132("div", { className: "ml-auto flex items-center gap-3", children: [
62685
- pendingCount > 0 && /* @__PURE__ */ jsxs132("span", { className: "text-xs text-sky-300", children: [
62686
- pendingCount,
62687
- " comment",
62688
- pendingCount > 1 ? "s" : "",
62689
- " pending \u2014 reply anything in your agent chat and it will apply them."
63314
+ submitError,
63315
+ messageCopied,
63316
+ onSave,
63317
+ onMessageCopied
63318
+ }) {
63319
+ return /* @__PURE__ */ jsxs132("div", { className: "ml-auto flex min-w-0 flex-1 flex-wrap items-center justify-end gap-2 sm:flex-none", children: [
63320
+ pendingCount > 0 && /* @__PURE__ */ jsxs132(Fragment46, { children: [
63321
+ /* @__PURE__ */ jsx155("span", { className: "text-xs text-sky-300", children: messageCopied ? "Feedback saved \xB7 Message copied \u2014 paste it in your terminal or IDE agent chat." : "Feedback saved \xB7 Agent not notified." }),
63322
+ /* @__PURE__ */ jsx155(
63323
+ AgentChatMessageButton,
63324
+ {
63325
+ message: APPLY_STORYBOARD_FEEDBACK_MESSAGE,
63326
+ label: messageCopied ? "Copy again" : "Copy prompt for agent",
63327
+ onCopied: onMessageCopied
63328
+ }
63329
+ )
62690
63330
  ] }),
62691
- /* @__PURE__ */ jsx154(
63331
+ pendingCount === 0 && draftCount === 0 && /* @__PURE__ */ jsx155("span", { className: "text-xs text-neutral-500", children: "Add frame comments to request changes." }),
63332
+ submitError && /* @__PURE__ */ jsxs132("span", { className: "max-w-64 truncate text-xs text-red-400", title: submitError, children: [
63333
+ "Couldn\u2019t submit: ",
63334
+ submitError
63335
+ ] }),
63336
+ draftCount > 0 && /* @__PURE__ */ jsxs132(
62692
63337
  Button,
62693
63338
  {
62694
63339
  variant: "primary",
62695
63340
  size: "sm",
62696
63341
  loading: submitState === "saving",
62697
- disabled: draftCount === 0 || submitState === "saving",
62698
- onClick: onSubmit,
62699
- children: draftCount > 0 ? `Submit comments (${draftCount})` : "Submit comments"
63342
+ disabled: submitState === "saving",
63343
+ onClick: onSave,
63344
+ children: [
63345
+ "Save & copy message (",
63346
+ draftCount,
63347
+ ")"
63348
+ ]
63349
+ }
63350
+ )
63351
+ ] });
63352
+ }
63353
+ function StoryboardWarnings({
63354
+ warnings,
63355
+ onOpenSource
63356
+ }) {
63357
+ if (warnings.length === 0) return null;
63358
+ return /* @__PURE__ */ jsxs132("details", { className: "mt-3 rounded-lg border border-amber-900/60 bg-amber-950/20 px-4 py-2 text-xs text-amber-200", children: [
63359
+ /* @__PURE__ */ jsxs132("summary", { className: "cursor-pointer font-medium", children: [
63360
+ warnings.length,
63361
+ " storyboard warning",
63362
+ warnings.length === 1 ? "" : "s"
63363
+ ] }),
63364
+ /* @__PURE__ */ jsx155("ul", { className: "mt-2 space-y-1 text-amber-200/80", children: warnings.map((warning, index) => /* @__PURE__ */ jsxs132("li", { children: [
63365
+ warning.line ? `Line ${warning.line}: ` : "",
63366
+ warning.message
63367
+ ] }, `${warning.line ?? "unknown"}-${index}`)) }),
63368
+ /* @__PURE__ */ jsx155(
63369
+ "button",
63370
+ {
63371
+ type: "button",
63372
+ onClick: onOpenSource,
63373
+ className: "mt-2 rounded text-amber-100 underline underline-offset-2 hover:text-white",
63374
+ children: "Open source to fix"
62700
63375
  }
62701
63376
  )
62702
63377
  ] });
@@ -62714,14 +63389,14 @@ function SubViewToggle({ value, onChange }) {
62714
63389
  const next = SUB_VIEWS[(currentIndex + delta + SUB_VIEWS.length) % SUB_VIEWS.length];
62715
63390
  if (next) onChange(next.value);
62716
63391
  };
62717
- return /* @__PURE__ */ jsx154(
63392
+ return /* @__PURE__ */ jsx155(
62718
63393
  "div",
62719
63394
  {
62720
63395
  className: "flex items-center gap-0.5 rounded-md bg-neutral-900 p-0.5",
62721
63396
  role: "tablist",
62722
63397
  "aria-label": "Storyboard view",
62723
63398
  onKeyDown: handleKeyDown,
62724
- children: SUB_VIEWS.map((option) => /* @__PURE__ */ jsx154(
63399
+ children: SUB_VIEWS.map((option) => /* @__PURE__ */ jsx155(
62725
63400
  "button",
62726
63401
  {
62727
63402
  type: "button",
@@ -62739,25 +63414,25 @@ function SubViewToggle({ value, onChange }) {
62739
63414
  }
62740
63415
 
62741
63416
  // src/components/storyboard/StoryboardView.tsx
62742
- import { jsx as jsx155, jsxs as jsxs133 } from "react/jsx-runtime";
63417
+ import { jsx as jsx156, jsxs as jsxs133 } from "react/jsx-runtime";
62743
63418
  function StoryboardView({ projectId, onSelectComposition }) {
62744
63419
  const { data, loading, error, reload } = useStoryboard(projectId);
62745
63420
  useProjectSignaturePoll(projectId, data?.signature, reload);
62746
- if (loading) return /* @__PURE__ */ jsx155(StoryboardFrame, { children: /* @__PURE__ */ jsx155(Message, { children: "Loading storyboard\u2026" }) });
63421
+ if (loading) return /* @__PURE__ */ jsx156(StoryboardFrame, { children: /* @__PURE__ */ jsx156(Message, { children: "Loading storyboard\u2026" }) });
62747
63422
  if (error) {
62748
63423
  return /* @__PURE__ */ jsxs133(StoryboardFrame, { children: [
62749
63424
  /* @__PURE__ */ jsxs133(Message, { tone: "error", children: [
62750
63425
  "Couldn\u2019t load the storyboard: ",
62751
63426
  error
62752
63427
  ] }),
62753
- /* @__PURE__ */ jsx155("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx155(Button, { size: "sm", variant: "secondary", onClick: reload, children: "Retry" }) })
63428
+ /* @__PURE__ */ jsx156("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx156(Button, { size: "sm", variant: "secondary", onClick: reload, children: "Retry" }) })
62754
63429
  ] });
62755
63430
  }
62756
- if (!data) return /* @__PURE__ */ jsx155(StoryboardFrame, { children: null });
63431
+ if (!data) return /* @__PURE__ */ jsx156(StoryboardFrame, { children: null });
62757
63432
  if (!data.exists) {
62758
- return /* @__PURE__ */ jsx155(StoryboardFrame, { children: /* @__PURE__ */ jsx155(EmptyState, { path: data.path }) });
63433
+ return /* @__PURE__ */ jsx156(StoryboardFrame, { children: /* @__PURE__ */ jsx156(EmptyState, { path: data.path }) });
62759
63434
  }
62760
- return /* @__PURE__ */ jsx155(
63435
+ return /* @__PURE__ */ jsx156(
62761
63436
  StoryboardLoaded,
62762
63437
  {
62763
63438
  projectId,
@@ -62768,10 +63443,10 @@ function StoryboardView({ projectId, onSelectComposition }) {
62768
63443
  );
62769
63444
  }
62770
63445
  function StoryboardFrame({ children }) {
62771
- return /* @__PURE__ */ jsx155("div", { className: "flex-1 min-h-0 overflow-auto bg-neutral-950 text-neutral-200", children: /* @__PURE__ */ jsx155("div", { className: "mx-auto max-w-[1400px] px-8 py-8", children }) });
63446
+ return /* @__PURE__ */ jsx156("div", { className: "flex-1 min-h-0 overflow-auto bg-neutral-950 text-neutral-200", children: /* @__PURE__ */ jsx156("div", { className: "mx-auto max-w-[1400px] px-8 py-8", children }) });
62772
63447
  }
62773
63448
  function Message({ children, tone = "muted" }) {
62774
- return /* @__PURE__ */ jsx155(
63449
+ return /* @__PURE__ */ jsx156(
62775
63450
  "div",
62776
63451
  {
62777
63452
  className: `px-6 py-12 text-center text-sm ${tone === "error" ? "text-red-400" : "text-neutral-500"}`,
@@ -62804,7 +63479,7 @@ Add one \`## Frame N\` section per beat. Keep the arc tight.
62804
63479
  Then run the review loop from the hyperframes-core skill (references/review-loop.md): present the plan as a proposal, offer wireframe sketches on this board, and build on the confirmed layouts.`;
62805
63480
  }
62806
63481
  function EmptyState({ path }) {
62807
- const [copied, setCopied] = useState117(false);
63482
+ const [copied, setCopied] = useState118(false);
62808
63483
  const prompt = handoffPrompt(path);
62809
63484
  const onCopy = async () => {
62810
63485
  if (await copyTextToClipboard(prompt)) {
@@ -62814,67 +63489,67 @@ function EmptyState({ path }) {
62814
63489
  };
62815
63490
  return /* @__PURE__ */ jsxs133("div", { children: [
62816
63491
  /* @__PURE__ */ jsxs133("div", { className: "rounded-lg border border-dashed border-neutral-800 px-6 py-10 text-center", children: [
62817
- /* @__PURE__ */ jsx155("h2", { className: "text-base font-semibold text-neutral-300", children: "No storyboard yet" }),
63492
+ /* @__PURE__ */ jsx156("h2", { className: "text-base font-semibold text-neutral-300", children: "No storyboard yet" }),
62818
63493
  /* @__PURE__ */ jsxs133("p", { className: "mx-auto mt-2 max-w-md text-sm text-neutral-500", children: [
62819
63494
  "Add a ",
62820
- /* @__PURE__ */ jsx155("code", { className: "rounded bg-neutral-900 px-1 py-0.5 text-neutral-400", children: path }),
63495
+ /* @__PURE__ */ jsx156("code", { className: "rounded bg-neutral-900 px-1 py-0.5 text-neutral-400", children: path }),
62821
63496
  " ",
62822
63497
  "at the project root to plan this video frame by frame. Hand this prompt to your coding agent to scaffold it."
62823
63498
  ] }),
62824
63499
  /* @__PURE__ */ jsxs133("div", { className: "mx-auto mt-6 max-w-2xl overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900 text-left", children: [
62825
63500
  /* @__PURE__ */ jsxs133("div", { className: "flex items-center justify-between border-b border-neutral-800 px-3 py-2", children: [
62826
- /* @__PURE__ */ jsx155("span", { className: "font-mono text-xs text-neutral-500", children: "Prompt for your agent" }),
62827
- /* @__PURE__ */ jsx155(
63501
+ /* @__PURE__ */ jsx156("span", { className: "font-mono text-xs text-neutral-500", children: "Prompt for your agent" }),
63502
+ /* @__PURE__ */ jsx156(
62828
63503
  Button,
62829
63504
  {
62830
63505
  size: "sm",
62831
63506
  variant: "secondary",
62832
63507
  onClick: onCopy,
62833
- icon: copied ? /* @__PURE__ */ jsx155(Check2, { size: 14 }) : /* @__PURE__ */ jsx155(Copy2, { size: 14 }),
63508
+ icon: copied ? /* @__PURE__ */ jsx156(Check2, { size: 14 }) : /* @__PURE__ */ jsx156(Copy2, { size: 14 }),
62834
63509
  children: copied ? "Copied" : "Copy prompt"
62835
63510
  }
62836
63511
  )
62837
63512
  ] }),
62838
- /* @__PURE__ */ jsx155("pre", { className: "max-h-64 overflow-auto px-4 py-3 font-mono text-xs leading-relaxed text-neutral-400 whitespace-pre-wrap", children: prompt })
63513
+ /* @__PURE__ */ jsx156("pre", { className: "max-h-64 overflow-auto px-4 py-3 font-mono text-xs leading-relaxed text-neutral-400 whitespace-pre-wrap", children: prompt })
62839
63514
  ] })
62840
63515
  ] }),
62841
- /* @__PURE__ */ jsx155(SkeletonPreview, {})
63516
+ /* @__PURE__ */ jsx156(SkeletonPreview, {})
62842
63517
  ] });
62843
63518
  }
62844
63519
  function SkeletonPreview() {
62845
63520
  return /* @__PURE__ */ jsxs133("div", { "aria-hidden": "true", className: "mt-10 select-none opacity-40", children: [
62846
- /* @__PURE__ */ jsx155("div", { className: "mb-4 text-center text-xs uppercase tracking-wide text-neutral-600", children: "Preview" }),
62847
- /* @__PURE__ */ jsx155("div", { className: "grid gap-x-6 gap-y-8 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs133("div", { className: "rounded-lg border border-neutral-800 bg-neutral-900/40 p-3", children: [
62848
- /* @__PURE__ */ jsx155("div", { className: "aspect-video w-full rounded bg-neutral-800/60" }),
62849
- /* @__PURE__ */ jsx155("div", { className: "mt-3 h-3 w-2/3 rounded bg-neutral-800/60" }),
62850
- /* @__PURE__ */ jsx155("div", { className: "mt-2 h-2.5 w-full rounded bg-neutral-800/40" }),
62851
- /* @__PURE__ */ jsx155("div", { className: "mt-1.5 h-2.5 w-4/5 rounded bg-neutral-800/40" })
63521
+ /* @__PURE__ */ jsx156("div", { className: "mb-4 text-center text-xs uppercase tracking-wide text-neutral-600", children: "Preview" }),
63522
+ /* @__PURE__ */ jsx156("div", { className: "grid gap-x-6 gap-y-8 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs133("div", { className: "rounded-lg border border-neutral-800 bg-neutral-900/40 p-3", children: [
63523
+ /* @__PURE__ */ jsx156("div", { className: "aspect-video w-full rounded bg-neutral-800/60" }),
63524
+ /* @__PURE__ */ jsx156("div", { className: "mt-3 h-3 w-2/3 rounded bg-neutral-800/60" }),
63525
+ /* @__PURE__ */ jsx156("div", { className: "mt-2 h-2.5 w-full rounded bg-neutral-800/40" }),
63526
+ /* @__PURE__ */ jsx156("div", { className: "mt-1.5 h-2.5 w-4/5 rounded bg-neutral-800/40" })
62852
63527
  ] }, i)) })
62853
63528
  ] });
62854
63529
  }
62855
63530
 
62856
63531
  // src/components/StudioSplash.tsx
62857
- import { jsx as jsx156, jsxs as jsxs134 } from "react/jsx-runtime";
63532
+ import { jsx as jsx157, jsxs as jsxs134 } from "react/jsx-runtime";
62858
63533
  function StudioSplash({ waiting }) {
62859
- return /* @__PURE__ */ jsx156("div", { className: "h-full w-full bg-neutral-950 flex items-center justify-center", children: waiting ? /* @__PURE__ */ jsxs134("div", { className: "flex flex-col items-center gap-3 text-center px-6", role: "status", children: [
62860
- /* @__PURE__ */ jsx156("div", { className: "w-4 h-4 rounded-full border-2 border-neutral-700 border-t-neutral-500 animate-spin motion-reduce:animate-none" }),
63534
+ return /* @__PURE__ */ jsx157("div", { className: "h-full w-full bg-neutral-950 flex items-center justify-center", children: waiting ? /* @__PURE__ */ jsxs134("div", { className: "flex flex-col items-center gap-3 text-center px-6", role: "status", children: [
63535
+ /* @__PURE__ */ jsx157("div", { className: "w-4 h-4 rounded-full border-2 border-neutral-700 border-t-neutral-500 animate-spin motion-reduce:animate-none" }),
62861
63536
  /* @__PURE__ */ jsxs134("p", { className: "text-xs text-neutral-600", children: [
62862
63537
  "Waiting for preview server\u2026 run",
62863
63538
  " ",
62864
- /* @__PURE__ */ jsx156("code", { className: "text-neutral-500 font-mono", children: "npm run dev" })
63539
+ /* @__PURE__ */ jsx157("code", { className: "text-neutral-500 font-mono", children: "npm run dev" })
62865
63540
  ] })
62866
63541
  ] }) : /* @__PURE__ */ jsxs134("div", { className: "flex flex-col items-center gap-3 text-center px-6", role: "status", children: [
62867
- /* @__PURE__ */ jsx156("div", { className: "w-4 h-4 rounded-full bg-studio-accent animate-pulse motion-reduce:animate-none" }),
62868
- /* @__PURE__ */ jsx156("p", { className: "text-xs text-neutral-600", children: "Connecting to project\u2026" })
63542
+ /* @__PURE__ */ jsx157("div", { className: "w-4 h-4 rounded-full bg-studio-accent animate-pulse motion-reduce:animate-none" }),
63543
+ /* @__PURE__ */ jsx157("p", { className: "text-xs text-neutral-600", children: "Connecting to project\u2026" })
62869
63544
  ] }) });
62870
63545
  }
62871
63546
 
62872
63547
  // src/hooks/useServerConnection.ts
62873
- import { useEffect as useEffect99, useState as useState118 } from "react";
63548
+ import { useEffect as useEffect101, useState as useState119 } from "react";
62874
63549
  function useServerConnection() {
62875
- const [projectId, setProjectId] = useState118(null);
62876
- const [resolving, setResolving] = useState118(true);
62877
- const [waitingForServer, setWaitingForServer] = useState118(false);
63550
+ const [projectId, setProjectId] = useState119(null);
63551
+ const [resolving, setResolving] = useState119(true);
63552
+ const [waitingForServer, setWaitingForServer] = useState119(false);
62878
63553
  useMountEffect(() => {
62879
63554
  const hashProjectId = parseProjectIdFromHash(window.location.hash);
62880
63555
  let cancelled = false;
@@ -62911,7 +63586,7 @@ function useServerConnection() {
62911
63586
  if (retryTimer !== null) clearTimeout(retryTimer);
62912
63587
  };
62913
63588
  });
62914
- useEffect99(() => {
63589
+ useEffect101(() => {
62915
63590
  const onHashChange = () => {
62916
63591
  const next = parseProjectIdFromHash(window.location.hash);
62917
63592
  if (next && next !== projectId) setProjectId(next);
@@ -62923,56 +63598,56 @@ function useServerConnection() {
62923
63598
  }
62924
63599
 
62925
63600
  // src/App.tsx
62926
- import { jsx as jsx157, jsxs as jsxs135 } from "react/jsx-runtime";
63601
+ import { jsx as jsx158, jsxs as jsxs135 } from "react/jsx-runtime";
62927
63602
  function StudioApp() {
62928
63603
  const { projectId, resolving, waitingForServer } = useServerConnection();
62929
- const initialUrlStateRef = useRef119(readStudioUrlStateFromWindow());
63604
+ const initialUrlStateRef = useRef120(readStudioUrlStateFromWindow());
62930
63605
  const viewModeValue = useViewModeState();
62931
- useEffect100(() => {
63606
+ useEffect102(() => {
62932
63607
  if (resolving || waitingForServer) return;
62933
63608
  if (hasFiredSessionStart()) return;
62934
63609
  markSessionStartFired();
62935
63610
  trackStudioSessionStart({ has_project: projectId != null });
62936
63611
  }, [projectId, resolving, waitingForServer]);
62937
- const [activeCompPath, setActiveCompPath] = useState119(null);
62938
- const [activeCompPathHydrated, setActiveCompPathHydrated] = useState119(
63612
+ const [activeCompPath, setActiveCompPath] = useState120(null);
63613
+ const [activeCompPathHydrated, setActiveCompPathHydrated] = useState120(
62939
63614
  () => initialUrlStateRef.current.activeCompPath == null
62940
63615
  );
62941
- const [compIdToSrc, setCompIdToSrc] = useState119(/* @__PURE__ */ new Map());
62942
- const [previewIframe, setPreviewIframe] = useState119(null);
62943
- const [compositionLoading, setCompositionLoading] = useState119(true);
62944
- const [refreshKey, setRefreshKey] = useState119(0);
63616
+ const [compIdToSrc, setCompIdToSrc] = useState120(/* @__PURE__ */ new Map());
63617
+ const [previewIframe, setPreviewIframe] = useState120(null);
63618
+ const [compositionLoading, setCompositionLoading] = useState120(true);
63619
+ const [refreshKey, setRefreshKey] = useState120(0);
62945
63620
  const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
62946
- const [blockPreview, setBlockPreview] = useState119(null);
62947
- const previewIframeRef = useRef119(null);
62948
- const activeCompPathRef = useRef119(activeCompPath);
63621
+ const [blockPreview, setBlockPreview] = useState120(null);
63622
+ const previewIframeRef = useRef120(null);
63623
+ const activeCompPathRef = useRef120(activeCompPath);
62949
63624
  activeCompPathRef.current = activeCompPath;
62950
- const leftSidebarRef = useRef119(null);
63625
+ const leftSidebarRef = useRef120(null);
62951
63626
  const renderQueue = useRenderQueue(projectId);
62952
63627
  const captionEditMode = useCaptionStore((s) => s.isEditMode);
62953
63628
  const captionHasSelection = useCaptionStore((s) => s.selectedSegmentIds.size > 0);
62954
63629
  const captionSync = useCaptionSync(projectId);
62955
63630
  const timelineElements = usePlayerStore((s) => s.elements);
62956
63631
  const setSelectedTimelineElementId = usePlayerStore((s) => s.setSelectedElementId);
62957
- const timelineDuration = usePlayerStore((s) => s.duration);
63632
+ const timelineDuration2 = usePlayerStore((s) => s.duration);
62958
63633
  const isPlaying = usePlayerStore((s) => s.isPlaying);
62959
63634
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
62960
63635
  const activePreviewUrl = activeCompPath ? `/api/projects/${projectId}/preview/comp/${activeCompPath}` : null;
62961
- const effectiveTimelineDuration = useMemo49(() => {
63636
+ const effectiveTimelineDuration = useMemo50(() => {
62962
63637
  const maxEnd = timelineElements.length > 0 ? Math.max(...timelineElements.map((el) => el.start + el.duration)) : 0;
62963
- return Math.max(timelineDuration, maxEnd);
62964
- }, [timelineDuration, timelineElements]);
63638
+ return Math.max(timelineDuration2, maxEnd);
63639
+ }, [timelineDuration2, timelineElements]);
62965
63640
  const { toasts, showToast, dismissToast } = useToast();
62966
63641
  const panelLayout = usePanelLayout({
62967
63642
  rightCollapsed: initialUrlStateRef.current.rightCollapsed,
62968
63643
  rightPanelTab: initialUrlStateRef.current.rightPanelTab
62969
63644
  });
62970
63645
  const editHistory = usePersistentEditHistory({ projectId });
62971
- const domEditSaveTimestampRef = useRef119(0);
62972
- const handleDomZIndexReorderCommitRef = useRef119(null);
62973
- const pendingTimelineEditPathRef = useRef119(/* @__PURE__ */ new Set());
62974
- const isGestureRecordingRef = useRef119(false);
62975
- const reloadPreview = useCallback141(() => setRefreshKey((k) => k + 1), []);
63646
+ const domEditSaveTimestampRef = useRef120(0);
63647
+ const handleDomZIndexReorderCommitRef = useRef120(null);
63648
+ const pendingTimelineEditPathRef = useRef120(/* @__PURE__ */ new Set());
63649
+ const isGestureRecordingRef = useRef120(false);
63650
+ const reloadPreview = useCallback142(() => setRefreshKey((k) => k + 1), []);
62976
63651
  const fileManager = useFileManager({
62977
63652
  projectId,
62978
63653
  showToast,
@@ -62980,7 +63655,7 @@ function StudioApp() {
62980
63655
  domEditSaveTimestampRef,
62981
63656
  setRefreshKey
62982
63657
  });
62983
- const masterCompPath = useMemo49(
63658
+ const masterCompPath = useMemo50(
62984
63659
  () => resolveMasterCompositionPath(fileManager.fileTree),
62985
63660
  [fileManager.fileTree]
62986
63661
  );
@@ -62990,7 +63665,7 @@ function StudioApp() {
62990
63665
  domEditSaveTimestampRef,
62991
63666
  masterCompPath
62992
63667
  );
62993
- useEffect100(() => {
63668
+ useEffect102(() => {
62994
63669
  if (activeCompPathHydrated) return;
62995
63670
  if (!fileManager.fileTreeLoaded) return;
62996
63671
  const nextCompPath = normalizeStudioCompositionPath(
@@ -63018,6 +63693,7 @@ function StudioApp() {
63018
63693
  timelineElements,
63019
63694
  showToast,
63020
63695
  writeProjectFile: fileManager.writeProjectFile,
63696
+ observeProjectFileVersion: fileManager.observeProjectFileVersion,
63021
63697
  recordEdit: editHistory.recordEdit,
63022
63698
  domEditSaveTimestampRef,
63023
63699
  reloadPreview,
@@ -63030,20 +63706,14 @@ function StudioApp() {
63030
63706
  forceReloadSdkSession: sdkHandle.forceReload,
63031
63707
  handleDomZIndexReorderCommitRef
63032
63708
  });
63033
- const handleTimelineElementsMove = useCallback141(
63709
+ const handleTimelineElementsMove = useCallback142(
63034
63710
  async (edits, coalesceKey, operation = "timing", coalesceMs) => {
63035
63711
  const deps = { handleTimelineGroupMove: timelineEditing.handleTimelineGroupMove };
63036
63712
  await persistTimelineMoveEditsAtomically(edits, coalesceKey, operation, deps, coalesceMs);
63037
63713
  },
63038
63714
  [timelineEditing.handleTimelineGroupMove]
63039
63715
  );
63040
- const handleAddAssetAtPlayhead = useCallback141(
63041
- (assetPath) => timelineEditing.handleTimelineAssetDrop(assetPath, {
63042
- start: usePlayerStore.getState().currentTime,
63043
- track: 0
63044
- }),
63045
- [timelineEditing]
63046
- );
63716
+ const handleAddAssetAtPlayhead = useAddAssetAtPlayhead(timelineEditing.handleTimelineAssetDrop);
63047
63717
  const {
63048
63718
  activeBlockParams,
63049
63719
  setActiveBlockParams,
@@ -63066,18 +63736,18 @@ function StudioApp() {
63066
63736
  setRightCollapsed: panelLayout.setRightCollapsed,
63067
63737
  setRightPanelTab: panelLayout.setRightPanelTab
63068
63738
  });
63069
- const clearDomSelectionRef = useRef119(() => {
63739
+ const clearDomSelectionRef = useRef120(() => {
63070
63740
  });
63071
- const domEditSelectionBridgeRef = useRef119(null);
63072
- const handleDomEditElementDeleteRef = useRef119(
63741
+ const domEditSelectionBridgeRef = useRef120(null);
63742
+ const handleDomEditElementDeleteRef = useRef120(
63073
63743
  async () => {
63074
63744
  }
63075
63745
  );
63076
63746
  const domEditDeleteBridge = (s) => handleDomEditElementDeleteRef.current(s);
63077
- const resetKeyframesRef = useRef119(() => false);
63078
- const deleteSelectedKeyframesRef = useRef119(() => {
63747
+ const resetKeyframesRef = useRef120(() => false);
63748
+ const deleteSelectedKeyframesRef = useRef120(() => {
63079
63749
  });
63080
- const invalidateGsapCacheRef = useRef119(() => {
63750
+ const invalidateGsapCacheRef = useRef120(() => {
63081
63751
  });
63082
63752
  const { handleCopy, handlePaste, handleCut } = useClipboard({
63083
63753
  projectId,
@@ -63119,7 +63789,7 @@ function StudioApp() {
63119
63789
  forceReloadSdkSession: sdkHandle.forceReload,
63120
63790
  onToggleRecording: STUDIO_KEYFRAMES_ENABLED ? () => handleToggleRecordingRef.current() : void 0
63121
63791
  });
63122
- const sidebarTabRef = useRef119({
63792
+ const sidebarTabRef = useRef120({
63123
63793
  select: (t) => leftSidebarRef.current?.selectTab(t),
63124
63794
  get: () => leftSidebarRef.current?.getTab() ?? "compositions"
63125
63795
  });
@@ -63207,9 +63877,9 @@ function StudioApp() {
63207
63877
  resetErrors: resetConsoleErrors
63208
63878
  } = useConsoleErrorCapture(previewIframe);
63209
63879
  const dragOverlay = useGlobalFileDrop(timelineEditing.handleTimelineFileDrop);
63210
- const handleToggleRecordingRef = useRef119(() => {
63880
+ const handleToggleRecordingRef = useRef120(() => {
63211
63881
  });
63212
- const domEditSessionRef = useRef119(domEditSession);
63882
+ const domEditSessionRef = useRef120(domEditSession);
63213
63883
  domEditSessionRef.current = domEditSession;
63214
63884
  const { gestureState, gestureRecording, handleToggleRecording } = useGestureCommit({
63215
63885
  domEditSessionRef,
@@ -63219,7 +63889,7 @@ function StudioApp() {
63219
63889
  });
63220
63890
  handleToggleRecordingRef.current = handleToggleRecording;
63221
63891
  const recordingToggle = STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : void 0;
63222
- const canvasRectRef = useRef119(null);
63892
+ const canvasRectRef = useRef120(null);
63223
63893
  useLayoutEffect4(() => {
63224
63894
  if (gestureState !== "recording" || !previewIframe) {
63225
63895
  canvasRectRef.current = null;
@@ -63228,7 +63898,7 @@ function StudioApp() {
63228
63898
  const r = previewIframe.getBoundingClientRect();
63229
63899
  canvasRectRef.current = { left: r.left, top: r.top, width: r.width, height: r.height };
63230
63900
  }, [gestureState, previewIframe]);
63231
- const handlePreviewIframeRef = useCallback141(
63901
+ const handlePreviewIframeRef = useCallback142(
63232
63902
  (iframe) => {
63233
63903
  previewIframeRef.current = iframe;
63234
63904
  setPreviewIframe(iframe);
@@ -63296,8 +63966,8 @@ function StudioApp() {
63296
63966
  handlePreviewIframeRef,
63297
63967
  refreshPreviewDocumentVersion
63298
63968
  });
63299
- const timelineToolbar = useMemo49(
63300
- () => /* @__PURE__ */ jsx157(
63969
+ const timelineToolbar = useMemo50(
63970
+ () => /* @__PURE__ */ jsx158(
63301
63971
  TimelineToolbar,
63302
63972
  {
63303
63973
  domEditSession,
@@ -63307,8 +63977,8 @@ function StudioApp() {
63307
63977
  [domEditSession, timelineEditing.handleTimelineElementSplit]
63308
63978
  );
63309
63979
  if (resolving || waitingForServer || !projectId)
63310
- return /* @__PURE__ */ jsx157(StudioSplash, { waiting: waitingForServer });
63311
- return /* @__PURE__ */ jsx157(StudioShellProvider, { value: studioCtxValue, children: /* @__PURE__ */ jsx157(StudioPlaybackProvider, { value: studioCtxValue, children: /* @__PURE__ */ jsx157(ViewModeProvider, { value: viewModeValue, children: /* @__PURE__ */ jsx157(PanelLayoutProvider, { value: panelLayout, children: /* @__PURE__ */ jsx157(FileManagerProvider, { value: fileManager, children: /* @__PURE__ */ jsx157(DomEditProvider, { value: domEditSession, children: /* @__PURE__ */ jsxs135(
63980
+ return /* @__PURE__ */ jsx158(StudioSplash, { waiting: waitingForServer });
63981
+ return /* @__PURE__ */ jsx158(StudioShellProvider, { value: studioCtxValue, children: /* @__PURE__ */ jsx158(StudioPlaybackProvider, { value: studioCtxValue, children: /* @__PURE__ */ jsx158(ViewModeProvider, { value: viewModeValue, children: /* @__PURE__ */ jsx158(PanelLayoutProvider, { value: panelLayout, children: /* @__PURE__ */ jsx158(FileManagerProvider, { value: fileManager, children: /* @__PURE__ */ jsx158(DomEditProvider, { value: domEditSession, children: /* @__PURE__ */ jsxs135(
63312
63982
  "div",
63313
63983
  {
63314
63984
  className: "flex flex-col h-full w-full bg-neutral-950 relative",
@@ -63317,7 +63987,7 @@ function StudioApp() {
63317
63987
  onDragLeave: dragOverlay.onDragLeave,
63318
63988
  onDrop: dragOverlay.onDrop,
63319
63989
  children: [
63320
- /* @__PURE__ */ jsx157(
63990
+ /* @__PURE__ */ jsx158(
63321
63991
  StudioHeader,
63322
63992
  {
63323
63993
  captureFrameHref: frameCapture.captureFrameHref,
@@ -63335,25 +64005,25 @@ function StudioApp() {
63335
64005
  }
63336
64006
  }
63337
64007
  ),
63338
- previewPersistence.domEditSaveQueuePaused && /* @__PURE__ */ jsx157(
64008
+ previewPersistence.domEditSaveQueuePaused && /* @__PURE__ */ jsx158(
63339
64009
  SaveQueuePausedBanner,
63340
64010
  {
63341
64011
  message: previewPersistence.domEditSaveQueuePaused,
63342
64012
  onRetry: previewPersistence.resetDomEditSaveQueueBreaker
63343
64013
  }
63344
64014
  ),
63345
- viewModeValue.viewMode === "storyboard" && /* @__PURE__ */ jsx157(
64015
+ viewModeValue.viewMode === "storyboard" && /* @__PURE__ */ jsx158(
63346
64016
  StoryboardView,
63347
64017
  {
63348
64018
  projectId,
63349
64019
  onSelectComposition: handleSelectComposition
63350
64020
  }
63351
64021
  ),
63352
- /* @__PURE__ */ jsx157(
64022
+ /* @__PURE__ */ jsx158(
63353
64023
  EditorShell,
63354
64024
  {
63355
64025
  hidden: viewModeValue.viewMode === "storyboard",
63356
- left: /* @__PURE__ */ jsx157(
64026
+ left: /* @__PURE__ */ jsx158(
63357
64027
  StudioLeftSidebar,
63358
64028
  {
63359
64029
  leftSidebarRef,
@@ -63367,7 +64037,7 @@ function StudioApp() {
63367
64037
  onAddAssetToTimeline: handleAddAssetAtPlayhead
63368
64038
  }
63369
64039
  ),
63370
- right: panelLayout.rightCollapsed ? null : /* @__PURE__ */ jsx157(
64040
+ right: panelLayout.rightCollapsed ? null : /* @__PURE__ */ jsx158(
63371
64041
  StudioRightPanel,
63372
64042
  {
63373
64043
  designPanelActive,
@@ -63381,6 +64051,7 @@ function StudioApp() {
63381
64051
  onToggleRecording: recordingToggle,
63382
64052
  sdkSession: sdkHandle.session,
63383
64053
  publishSdkSession: sdkHandle.publish,
64054
+ forceReloadSdkSession: sdkHandle.forceReload,
63384
64055
  reloadPreview,
63385
64056
  domEditSaveTimestampRef,
63386
64057
  recordEdit: editHistory.recordEdit,
@@ -63410,7 +64081,7 @@ function StudioApp() {
63410
64081
  recordingState: gestureState,
63411
64082
  onToggleRecording: recordingToggle,
63412
64083
  blockPreview,
63413
- gestureOverlay: gestureState === "recording" && previewIframe ? /* @__PURE__ */ jsx157(
64084
+ gestureOverlay: gestureState === "recording" && previewIframe ? /* @__PURE__ */ jsx158(
63414
64085
  GestureTrailOverlay,
63415
64086
  {
63416
64087
  samples: gestureRecording.samplesRef.current,
@@ -63423,7 +64094,7 @@ function StudioApp() {
63423
64094
  ) : void 0
63424
64095
  }
63425
64096
  ),
63426
- /* @__PURE__ */ jsx157(
64097
+ /* @__PURE__ */ jsx158(
63427
64098
  StudioOverlays,
63428
64099
  {
63429
64100
  projectId,
@@ -63445,32 +64116,32 @@ function StudioApp() {
63445
64116
  }
63446
64117
 
63447
64118
  // src/hooks/useElementPicker.ts
63448
- import { useState as useState120, useCallback as useCallback142, useRef as useRef120 } from "react";
64119
+ import { useState as useState121, useCallback as useCallback143, useRef as useRef121 } from "react";
63449
64120
  function useElementPicker(iframeRef, options) {
63450
- const [isPickMode, setIsPickMode] = useState120(false);
63451
- const [pickedElement, setPickedElement] = useState120(null);
63452
- const activeOverrideRef = useRef120(null);
63453
- const getActiveIframe = useCallback142(() => {
64121
+ const [isPickMode, setIsPickMode] = useState121(false);
64122
+ const [pickedElement, setPickedElement] = useState121(null);
64123
+ const activeOverrideRef = useRef121(null);
64124
+ const getActiveIframe = useCallback143(() => {
63454
64125
  return activeOverrideRef.current ?? iframeRef.current;
63455
64126
  }, [iframeRef]);
63456
- const setActiveIframe = useCallback142((el) => {
64127
+ const setActiveIframe = useCallback143((el) => {
63457
64128
  activeOverrideRef.current = el;
63458
64129
  }, []);
63459
- const enablePick = useCallback142(() => {
64130
+ const enablePick = useCallback143(() => {
63460
64131
  try {
63461
64132
  postRuntimeControlMessage(getActiveIframe()?.contentWindow, "enable-pick-mode");
63462
64133
  setIsPickMode(true);
63463
64134
  } catch {
63464
64135
  }
63465
64136
  }, [getActiveIframe]);
63466
- const disablePick = useCallback142(() => {
64137
+ const disablePick = useCallback143(() => {
63467
64138
  try {
63468
64139
  postRuntimeControlMessage(getActiveIframe()?.contentWindow, "disable-pick-mode");
63469
64140
  } catch {
63470
64141
  }
63471
64142
  setIsPickMode(false);
63472
64143
  }, [getActiveIframe]);
63473
- const clearPick = useCallback142(() => {
64144
+ const clearPick = useCallback143(() => {
63474
64145
  setPickedElement(null);
63475
64146
  }, []);
63476
64147
  useMountEffect(() => {
@@ -63523,9 +64194,9 @@ function useElementPicker(iframeRef, options) {
63523
64194
  window.addEventListener("message", handleMessage);
63524
64195
  return () => window.removeEventListener("message", handleMessage);
63525
64196
  });
63526
- const optionsRef = useRef120(options);
64197
+ const optionsRef = useRef121(options);
63527
64198
  optionsRef.current = options;
63528
- const syncToSource = useCallback142(
64199
+ const syncToSource = useCallback143(
63529
64200
  (elementId, selector, op) => {
63530
64201
  const opts = optionsRef.current;
63531
64202
  if (!opts?.workspaceFiles || !opts.onSyncFiles || !elementId) return;
@@ -63541,7 +64212,7 @@ function useElementPicker(iframeRef, options) {
63541
64212
  },
63542
64213
  []
63543
64214
  );
63544
- const setStyle = useCallback142(
64215
+ const setStyle = useCallback143(
63545
64216
  (prop, value) => {
63546
64217
  const activeIframe = getActiveIframe();
63547
64218
  if (!pickedElement?.selector || !activeIframe) return;
@@ -63583,7 +64254,7 @@ function useElementPicker(iframeRef, options) {
63583
64254
  },
63584
64255
  [pickedElement, getActiveIframe, syncToSource]
63585
64256
  );
63586
- const setDataAttr = useCallback142(
64257
+ const setDataAttr = useCallback143(
63587
64258
  (attr, value) => {
63588
64259
  const activeIframe = getActiveIframe();
63589
64260
  if (!pickedElement?.selector || !activeIframe) return;
@@ -63611,7 +64282,7 @@ function useElementPicker(iframeRef, options) {
63611
64282
  },
63612
64283
  [pickedElement, getActiveIframe, syncToSource]
63613
64284
  );
63614
- const setTextContent = useCallback142(
64285
+ const setTextContent = useCallback143(
63615
64286
  (text) => {
63616
64287
  const activeIframe = getActiveIframe();
63617
64288
  if (!pickedElement?.selector || !activeIframe) return;
@@ -63634,7 +64305,7 @@ function useElementPicker(iframeRef, options) {
63634
64305
  },
63635
64306
  [pickedElement, getActiveIframe, syncToSource]
63636
64307
  );
63637
- const activeIframeRef = useRef120(null);
64308
+ const activeIframeRef = useRef121(null);
63638
64309
  activeIframeRef.current = getActiveIframe();
63639
64310
  return {
63640
64311
  isPickMode,