@hyperframes/studio 0.7.59 → 0.7.60

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 (45) hide show
  1. package/dist/assets/{hyperframes-player-Bm07FLkl.js → hyperframes-player-3XTTaVNf.js} +1 -1
  2. package/dist/assets/{index-B995FG46.js → index-D6etaey-.js} +1 -1
  3. package/dist/assets/index-DXbu6IPT.css +1 -0
  4. package/dist/assets/{index-FvzmPhfG.js → index-Dh_WhagG.js} +1 -1
  5. package/dist/assets/{index-CrkAdJkb.js → index-cH6NfVV_.js} +198 -198
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +250 -108
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +1 -1
  11. package/src/components/StudioRightPanel.tsx +13 -2
  12. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +2 -2
  13. package/src/components/editor/propertyPanelFlatMotionSection.tsx +1 -1
  14. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -2
  15. package/src/components/editor/propertyPanelFlatSelectRow.tsx +7 -1
  16. package/src/components/editor/propertyPanelFlatTextSection.tsx +1 -1
  17. package/src/contexts/FileManagerContext.tsx +3 -0
  18. package/src/contexts/PanelLayoutContext.tsx +3 -0
  19. package/src/hooks/timelineEditingHelpers.ts +2 -2
  20. package/src/hooks/useDomEditCommits.test.tsx +41 -1
  21. package/src/hooks/useDomEditCommits.ts +2 -2
  22. package/src/hooks/useDomEditSession.ts +1 -1
  23. package/src/hooks/useEditorSave.ts +1 -1
  24. package/src/hooks/useFileManager.projectOwnership.test.tsx +14 -3
  25. package/src/hooks/useFileManager.ts +76 -12
  26. package/src/hooks/usePanelLayout.test.ts +118 -0
  27. package/src/hooks/usePanelLayout.ts +24 -1
  28. package/src/hooks/usePreviewPersistence.ts +4 -1
  29. package/src/hooks/useRazorSplit.history.test.tsx +12 -4
  30. package/src/hooks/useRazorSplit.test.tsx +88 -0
  31. package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
  32. package/src/hooks/useRazorSplit.ts +50 -16
  33. package/src/hooks/useTimelineEditing.ts +2 -0
  34. package/src/hooks/useTimelineEditingTypes.ts +2 -1
  35. package/src/hooks/useTimelineGroupEditing.ts +1 -1
  36. package/src/utils/domEditSaveQueue.test.ts +19 -0
  37. package/src/utils/domEditSaveQueue.ts +2 -1
  38. package/src/utils/sdkCutover.test.ts +21 -5
  39. package/src/utils/sdkEditTransaction.ts +5 -4
  40. package/src/utils/studioFileHistory.ts +6 -4
  41. package/src/utils/studioFileVersion.test.ts +36 -0
  42. package/src/utils/studioFileVersion.ts +23 -0
  43. package/src/utils/studioSaveDiagnostics.test.ts +18 -0
  44. package/src/utils/studioSaveDiagnostics.ts +21 -0
  45. package/dist/assets/index-Dj5p8U_A.css +0 -1
package/dist/index.js CHANGED
@@ -2915,6 +2915,7 @@ function FileManagerProvider({
2915
2915
  readProjectFile,
2916
2916
  writeProjectFile,
2917
2917
  readOptionalProjectFile,
2918
+ observeProjectFileVersion,
2918
2919
  updateEditingFileContent,
2919
2920
  revealSourceOffset,
2920
2921
  openSourceForSelection,
@@ -2951,6 +2952,7 @@ function FileManagerProvider({
2951
2952
  readProjectFile,
2952
2953
  writeProjectFile,
2953
2954
  readOptionalProjectFile,
2955
+ observeProjectFileVersion,
2954
2956
  updateEditingFileContent,
2955
2957
  revealSourceOffset,
2956
2958
  openSourceForSelection,
@@ -2984,6 +2986,7 @@ function FileManagerProvider({
2984
2986
  readProjectFile,
2985
2987
  writeProjectFile,
2986
2988
  readOptionalProjectFile,
2989
+ observeProjectFileVersion,
2987
2990
  updateEditingFileContent,
2988
2991
  revealSourceOffset,
2989
2992
  openSourceForSelection,
@@ -6920,6 +6923,20 @@ var StudioSaveNetworkError = class extends Error {
6920
6923
  this.name = "StudioSaveNetworkError";
6921
6924
  }
6922
6925
  };
6926
+ var StudioFileConflictError = class extends StudioSaveHttpError {
6927
+ filePath;
6928
+ currentVersion;
6929
+ currentContent;
6930
+ attemptedContent;
6931
+ constructor(input) {
6932
+ super(`Save conflict: ${input.filePath} changed outside this Studio session`, 409);
6933
+ this.name = "StudioFileConflictError";
6934
+ this.filePath = input.filePath;
6935
+ this.currentVersion = input.currentVersion;
6936
+ this.currentContent = input.currentContent;
6937
+ this.attemptedContent = input.attemptedContent;
6938
+ }
6939
+ };
6923
6940
  function readNumericProperty(value, key) {
6924
6941
  const record = value;
6925
6942
  const property = record[key];
@@ -7093,14 +7110,14 @@ async function saveProjectFilesWithHistory({
7093
7110
  const writtenPaths = [];
7094
7111
  try {
7095
7112
  for (const path of changedPaths) {
7096
- await writeFile(path, snapshots[path].after);
7113
+ await writeFile(path, snapshots[path].after, snapshots[path].before);
7097
7114
  writtenPaths.push(path);
7098
7115
  }
7099
7116
  await recordEdit({ label, kind, coalesceKey, coalesceMs, files: snapshots });
7100
7117
  } catch (error) {
7101
7118
  try {
7102
7119
  for (const path of writtenPaths.reverse()) {
7103
- await writeFile(path, snapshots[path].before);
7120
+ await writeFile(path, snapshots[path].before, snapshots[path].after);
7104
7121
  }
7105
7122
  } catch (rollbackError) {
7106
7123
  throw new AggregateError(
@@ -24480,11 +24497,11 @@ async function buildCandidateEdit(live, deps, mutate, sourceSnapshot) {
24480
24497
  function isCutoverResult(value) {
24481
24498
  return "status" in value;
24482
24499
  }
24483
- async function rollbackWrite(targetPath, originalContent, deps, cause) {
24500
+ async function rollbackWrite(targetPath, originalContent, expectedCurrentContent, deps, cause) {
24484
24501
  try {
24485
24502
  deps.domEditSaveTimestampRef.current = Date.now();
24486
24503
  markSelfWrite(targetPath, originalContent);
24487
- await deps.writeProjectFile(targetPath, originalContent);
24504
+ await deps.writeProjectFile(targetPath, originalContent, expectedCurrentContent);
24488
24505
  return cause;
24489
24506
  } catch (rollbackError) {
24490
24507
  return new AggregateError(
@@ -24497,7 +24514,7 @@ async function writeAndRecord(after, targetPath, originalContent, deps, options)
24497
24514
  deps.domEditSaveTimestampRef.current = Date.now();
24498
24515
  markSelfWrite(targetPath, after);
24499
24516
  try {
24500
- await deps.writeProjectFile(targetPath, after);
24517
+ await deps.writeProjectFile(targetPath, after, originalContent);
24501
24518
  } catch (error) {
24502
24519
  return asCutoverError(error);
24503
24520
  }
@@ -24511,7 +24528,7 @@ async function writeAndRecord(after, targetPath, originalContent, deps, options)
24511
24528
  });
24512
24529
  return null;
24513
24530
  } catch (error) {
24514
- return rollbackWrite(targetPath, originalContent, deps, asCutoverError(error));
24531
+ return rollbackWrite(targetPath, originalContent, after, deps, asCutoverError(error));
24515
24532
  }
24516
24533
  }
24517
24534
  function refreshCommittedEdit(after, deps, options) {
@@ -36177,33 +36194,39 @@ function FlatSelectRow({
36177
36194
  return /* @__PURE__ */ jsxs75("div", { className: "group flex min-h-[30px] items-center justify-between", children: [
36178
36195
  /* @__PURE__ */ jsx88("span", { className: `text-[11px] ${VALUE_TIER_LABEL_CLASS[tier]}`, children: label }),
36179
36196
  /* @__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
- ] }),
36197
+ /* @__PURE__ */ jsxs75(
36198
+ "label",
36199
+ {
36200
+ 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"}`,
36201
+ children: [
36202
+ /* @__PURE__ */ jsx88(
36203
+ "select",
36204
+ {
36205
+ value,
36206
+ disabled,
36207
+ "aria-label": ariaLabel || label || void 0,
36208
+ onChange: (e) => {
36209
+ track("select", trackName);
36210
+ onChange(e.target.value);
36211
+ },
36212
+ className: `appearance-none bg-transparent text-right font-mono text-[11px] outline-none disabled:cursor-not-allowed ${VALUE_TIER_VALUE_CLASS[tier]}`,
36213
+ children: renderedOptions.map((option) => /* @__PURE__ */ jsx88("option", { value: option.value, children: option.label }, option.value))
36214
+ }
36215
+ ),
36216
+ /* @__PURE__ */ jsx88(
36217
+ "svg",
36218
+ {
36219
+ width: "10",
36220
+ height: "10",
36221
+ viewBox: "0 0 10 10",
36222
+ fill: "currentColor",
36223
+ className: "flex-shrink-0 text-panel-text-5",
36224
+ children: /* @__PURE__ */ jsx88("path", { d: "M2 3l3 4 3-4z" })
36225
+ }
36226
+ )
36227
+ ]
36228
+ }
36229
+ ),
36207
36230
  tier === "explicitCustom" && onReset && /* @__PURE__ */ jsx88(
36208
36231
  "button",
36209
36232
  {
@@ -36244,7 +36267,7 @@ function FlatRow({
36244
36267
  "span",
36245
36268
  {
36246
36269
  "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"}`,
36270
+ 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
36271
  children: /* @__PURE__ */ jsx89(
36249
36272
  CommitField,
36250
36273
  {
@@ -36680,7 +36703,7 @@ function FlatTextFieldEditor({
36680
36703
  children: "Weight"
36681
36704
  }
36682
36705
  ),
36683
- /* @__PURE__ */ jsxs77("label", { className: "flex items-center gap-1.5", children: [
36706
+ /* @__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
36707
  /* @__PURE__ */ jsx90(
36685
36708
  "select",
36686
36709
  {
@@ -37912,7 +37935,7 @@ function FlatTimingRow({
37912
37935
  };
37913
37936
  const cell = (label, value, onCommit) => /* @__PURE__ */ jsxs82("div", { className: "grid gap-px", children: [
37914
37937
  /* @__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(
37938
+ /* @__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
37939
  CommitField,
37917
37940
  {
37918
37941
  value,
@@ -38724,7 +38747,7 @@ function FlatColorGradingSection({
38724
38747
  track("select", "Custom LUT");
38725
38748
  applyLut(src || null, src && lut?.src === src ? lut.intensity : 1);
38726
38749
  },
38727
- className: "bg-transparent font-mono text-[10px] text-panel-text-3 outline-none",
38750
+ 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
38751
  children: [
38729
38752
  /* @__PURE__ */ jsx98("option", { value: "", children: "None" }),
38730
38753
  lutAssets.map((asset) => /* @__PURE__ */ jsx98("option", { value: asset, children: asset.split("/").pop() ?? asset }, asset))
@@ -38876,7 +38899,7 @@ function FlatColorGradingSection({
38876
38899
  onSetApplyScope(e.target.value);
38877
38900
  },
38878
38901
  disabled: applyBusy,
38879
- className: "bg-transparent font-mono text-[11px] text-panel-text-0 outline-none disabled:opacity-50",
38902
+ 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
38903
  children: [
38881
38904
  /* @__PURE__ */ jsx98("option", { value: "source-file", children: "Current file media" }),
38882
38905
  /* @__PURE__ */ jsx98("option", { value: "project", children: "All project media" })
@@ -40765,7 +40788,7 @@ var FileTree = memo33(function FileTree2({
40765
40788
  });
40766
40789
 
40767
40790
  // 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";
40791
+ import { useState as useState119, useCallback as useCallback141, useRef as useRef119, useMemo as useMemo50, useEffect as useEffect100, useLayoutEffect as useLayoutEffect4 } from "react";
40769
40792
 
40770
40793
  // src/components/renders/useRenderQueue.ts
40771
40794
  import { useState as useState64, useEffect as useEffect52, useCallback as useCallback58, useRef as useRef63, useMemo as useMemo31 } from "react";
@@ -42174,7 +42197,9 @@ function usePanelLayout(initialState2) {
42174
42197
  const trackedSetRightPanelTab = useCallback62(
42175
42198
  (tab) => {
42176
42199
  if (tab === "design" || tab === "layers") {
42177
- setRightInspectorPanes((panes) => ({ ...panes, [tab]: true }));
42200
+ setRightInspectorPanes(
42201
+ STUDIO_FLAT_INSPECTOR_ENABLED ? { design: tab === "design", layers: tab === "layers" } : (panes) => ({ ...panes, [tab]: true })
42202
+ );
42178
42203
  }
42179
42204
  setRightPanelTab(tab);
42180
42205
  trackStudioEvent("tab_switch", { panel: "right_panel", tab });
@@ -42188,6 +42213,9 @@ function usePanelLayout(initialState2) {
42188
42213
  return next;
42189
42214
  });
42190
42215
  }, []);
42216
+ const setExclusiveRightInspectorPane = useCallback62((pane) => {
42217
+ setRightInspectorPanes({ design: pane === "design", layers: pane === "layers" });
42218
+ }, []);
42191
42219
  return {
42192
42220
  leftWidth,
42193
42221
  setLeftWidth,
@@ -42201,6 +42229,7 @@ function usePanelLayout(initialState2) {
42201
42229
  setRightPanelTab: trackedSetRightPanelTab,
42202
42230
  rightInspectorPanes,
42203
42231
  toggleRightInspectorPane,
42232
+ setExclusiveRightInspectorPane,
42204
42233
  toggleLeftSidebar,
42205
42234
  handlePanelResizeStart,
42206
42235
  handlePanelResizeMove,
@@ -42209,7 +42238,24 @@ function usePanelLayout(initialState2) {
42209
42238
  }
42210
42239
 
42211
42240
  // src/hooks/useFileManager.ts
42212
- import { useState as useState70, useCallback as useCallback65, useRef as useRef71 } from "react";
42241
+ import { useState as useState70, useCallback as useCallback65, useMemo as useMemo34, useRef as useRef71 } from "react";
42242
+
42243
+ // src/utils/studioFileVersion.ts
42244
+ async function studioFileContentVersion(content) {
42245
+ const bytes = new TextEncoder().encode(content);
42246
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
42247
+ const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(
42248
+ ""
42249
+ );
42250
+ return `"sha256:${hex}"`;
42251
+ }
42252
+ async function studioExpectedFileVersion(versions, path, expectedContent) {
42253
+ if (expectedContent !== void 0) return studioFileContentVersion(expectedContent);
42254
+ return versions.get(path);
42255
+ }
42256
+ function createStudioWriteToken() {
42257
+ return globalThis.crypto.randomUUID();
42258
+ }
42213
42259
 
42214
42260
  // src/hooks/useFileTree.ts
42215
42261
  import { useState as useState69, useCallback as useCallback63, useEffect as useEffect55, useMemo as useMemo33 } from "react";
@@ -42354,6 +42400,17 @@ function useFileManager({
42354
42400
  const projectIdRef = useRef71(projectId);
42355
42401
  projectIdRef.current = projectId;
42356
42402
  const importedFontAssetsRef = useRef71([]);
42403
+ const fileVersionScope = useMemo34(
42404
+ () => ({ projectId, versions: /* @__PURE__ */ new Map() }),
42405
+ [projectId]
42406
+ );
42407
+ const fileVersions = fileVersionScope.versions;
42408
+ const observeProjectFileVersion = useCallback65(
42409
+ (path, version) => {
42410
+ fileVersions.set(path, version);
42411
+ },
42412
+ [fileVersions]
42413
+ );
42357
42414
  const {
42358
42415
  projectDir,
42359
42416
  fileTree,
@@ -42373,14 +42430,35 @@ function useFileManager({
42373
42430
  if (!response.ok) throw new Error(`Failed to read ${path}`);
42374
42431
  const data = await response.json();
42375
42432
  if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
42433
+ fileVersions.set(path, data.version ?? response.headers.get("etag"));
42376
42434
  return data.content;
42377
42435
  },
42378
- [projectId]
42436
+ [fileVersions, projectId]
42379
42437
  );
42380
42438
  const writeProjectFile = useCallback65(
42381
- async (path, content) => {
42439
+ async (path, content, expectedContent) => {
42382
42440
  if (!projectId) throw new Error("No active project");
42383
42441
  const writeProjectId = projectId;
42442
+ let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
42443
+ if (expectedVersion === void 0) {
42444
+ const preflight = await fetch(
42445
+ `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`
42446
+ );
42447
+ if (preflight.ok) {
42448
+ const data = await preflight.json();
42449
+ throw new StudioFileConflictError({
42450
+ filePath: path,
42451
+ currentVersion: data.version ?? preflight.headers.get("etag"),
42452
+ currentContent: data.content ?? null,
42453
+ attemptedContent: content
42454
+ });
42455
+ } else if (preflight.status === 404) {
42456
+ expectedVersion = null;
42457
+ } else {
42458
+ throw await createStudioSaveHttpError(preflight, `Failed to read ${path} before save`);
42459
+ }
42460
+ }
42461
+ const writeToken = createStudioWriteToken();
42384
42462
  await retryStudioSave(async () => {
42385
42463
  let response;
42386
42464
  try {
@@ -42388,7 +42466,11 @@ function useFileManager({
42388
42466
  `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
42389
42467
  {
42390
42468
  method: "PUT",
42391
- headers: { "Content-Type": "text/plain" },
42469
+ headers: {
42470
+ "Content-Type": "text/plain",
42471
+ "X-Hyperframes-Write-Token": writeToken,
42472
+ ...expectedVersion ? { "If-Match": expectedVersion } : { "If-None-Match": "*" }
42473
+ },
42392
42474
  body: content
42393
42475
  }
42394
42476
  );
@@ -42397,13 +42479,32 @@ function useFileManager({
42397
42479
  cause: error
42398
42480
  });
42399
42481
  }
42482
+ if (response.status === 409) {
42483
+ const conflict = await response.json().catch(() => null);
42484
+ const currentVersion = conflict?.currentVersion ?? null;
42485
+ if (currentVersion && conflict?.currentContent === content) {
42486
+ fileVersions.set(path, currentVersion);
42487
+ return;
42488
+ }
42489
+ throw new StudioFileConflictError({
42490
+ filePath: path,
42491
+ currentVersion,
42492
+ currentContent: conflict?.currentContent ?? null,
42493
+ attemptedContent: content
42494
+ });
42495
+ }
42400
42496
  if (!response.ok) throw await createStudioSaveHttpError(response, `Failed to save ${path}`);
42497
+ const result = await response.json();
42498
+ const version = result.version ?? response.headers.get("etag");
42499
+ if (!version)
42500
+ throw new Error(`Save response for ${path} did not include a content version`);
42501
+ fileVersions.set(path, version);
42401
42502
  });
42402
42503
  if (projectIdRef.current === writeProjectId && editingPathRef.current === path) {
42403
42504
  setEditingFile({ path, content });
42404
42505
  }
42405
42506
  },
42406
- [projectId]
42507
+ [fileVersions, projectId]
42407
42508
  );
42408
42509
  const updateEditingFileContent = useCallback65((path, content) => {
42409
42510
  if (editingPathRef.current === path) {
@@ -42418,9 +42519,10 @@ function useFileManager({
42418
42519
  );
42419
42520
  if (!response.ok) throw new Error(`Failed to read ${path}`);
42420
42521
  const data = await response.json();
42522
+ fileVersions.set(path, data.version ?? response.headers.get("etag"));
42421
42523
  return typeof data.content === "string" ? data.content : "";
42422
42524
  },
42423
- [projectId]
42525
+ [fileVersions, projectId]
42424
42526
  );
42425
42527
  const { saveRafRef, handleContentChange } = useEditorSave({
42426
42528
  editingPathRef,
@@ -42450,13 +42552,14 @@ function useFileManager({
42450
42552
  return r.json();
42451
42553
  }).then((data) => {
42452
42554
  if (data.content != null) {
42555
+ fileVersions.set(path, data.version ?? null);
42453
42556
  setEditingFile({ path, content: data.content });
42454
42557
  }
42455
42558
  }).catch((err) => {
42456
42559
  showToast(err instanceof Error ? err.message : `Failed to load ${path}`, "error");
42457
42560
  });
42458
42561
  },
42459
- [showToast]
42562
+ [fileVersions, showToast]
42460
42563
  );
42461
42564
  const openSourceForSelection = useCallback65(
42462
42565
  (sourceFile, target) => {
@@ -42477,6 +42580,7 @@ function useFileManager({
42477
42580
  }).then((r) => r.json()).then((data) => {
42478
42581
  if (requestId !== revealRequestIdRef.current) return;
42479
42582
  if (data.content != null) {
42583
+ fileVersions.set(sourceFile, data.version ?? null);
42480
42584
  setEditingFile({ path: sourceFile, content: data.content });
42481
42585
  const match = findTagByTarget(data.content, target);
42482
42586
  setRevealSourceOffset(match ? match.start : null);
@@ -42484,7 +42588,7 @@ function useFileManager({
42484
42588
  }).catch(() => {
42485
42589
  });
42486
42590
  },
42487
- [editingFile?.content]
42591
+ [editingFile?.content, fileVersions]
42488
42592
  );
42489
42593
  const uploadProjectFiles = useCallback65(
42490
42594
  async (files, dir) => {
@@ -42689,6 +42793,7 @@ function useFileManager({
42689
42793
  readProjectFile,
42690
42794
  writeProjectFile,
42691
42795
  readOptionalProjectFile,
42796
+ observeProjectFileVersion,
42692
42797
  updateEditingFileContent,
42693
42798
  // Click-to-source
42694
42799
  revealSourceOffset,
@@ -42751,7 +42856,8 @@ function createDomEditSaveQueue(options = {}) {
42751
42856
  return result;
42752
42857
  } catch (error) {
42753
42858
  consecutiveFailures += 1;
42754
- if (consecutiveFailures >= failureThreshold) open(error);
42859
+ if (getStudioSaveStatusCode(error) === 409 || consecutiveFailures >= failureThreshold)
42860
+ open(error);
42755
42861
  throw error;
42756
42862
  }
42757
42863
  };
@@ -42991,7 +43097,7 @@ function usePreviewPersistence({
42991
43097
  if (!domEditSaveQueueRef.current) {
42992
43098
  domEditSaveQueueRef.current = createDomEditSaveQueue({
42993
43099
  onOpen: (event) => {
42994
- const message = "Auto-save is paused. Check your connection.";
43100
+ 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
43101
  setDomEditSaveQueuePaused(message);
42996
43102
  showToastRef.current(message, "error");
42997
43103
  trackStudioEvent("save_queue_paused", {
@@ -43136,7 +43242,10 @@ async function splitHtmlElement(projectId, targetPath, patchTarget, splitTime, n
43136
43242
  }
43137
43243
  );
43138
43244
  if (!response.ok) throw new Error("Split request failed");
43139
- return await response.json();
43245
+ const data = await response.json();
43246
+ const version = data.version ?? response.headers.get("etag");
43247
+ if (!version) throw new Error("Split response did not include a content version");
43248
+ return { ...data, version };
43140
43249
  }
43141
43250
  async function splitGsapAnimations(projectId, targetPath, originalId, newId, splitTime, elementStart, elementDuration) {
43142
43251
  const response = await fetch(
@@ -43164,6 +43273,7 @@ async function splitGsapAnimations(projectId, targetPath, originalId, newId, spl
43164
43273
  const data = await response.json();
43165
43274
  return {
43166
43275
  content: data.ok && data.after ? data.after : null,
43276
+ version: data.version ?? response.headers.get("etag") ?? void 0,
43167
43277
  skippedSelectors: data.skippedSelectors
43168
43278
  };
43169
43279
  }
@@ -43174,9 +43284,9 @@ function getOriginalContent(originals, path) {
43174
43284
  }
43175
43285
  return original;
43176
43286
  }
43177
- async function restoreFilesToOriginal(originals, paths, writeProjectFile) {
43178
- for (const path of paths) {
43179
- await writeProjectFile(path, getOriginalContent(originals, path));
43287
+ async function restoreFilesToOriginal(originals, snapshots, writeProjectFile) {
43288
+ for (const [path, snapshot] of snapshots) {
43289
+ await writeProjectFile(path, getOriginalContent(originals, path), snapshot.after);
43180
43290
  }
43181
43291
  }
43182
43292
  async function readOriginalFiles(pid, elements, activeCompPath) {
@@ -43189,21 +43299,28 @@ async function readOriginalFiles(pid, elements, activeCompPath) {
43189
43299
  }
43190
43300
  return originals;
43191
43301
  }
43192
- async function splitElementsAtTime(pid, elements, splitTime, activeCompPath, originals, snapshots, writeProjectFile) {
43302
+ async function splitElementsAtTime(pid, elements, splitTime, activeCompPath, originals, snapshots, writeProjectFile, observeProjectFileVersion) {
43193
43303
  let count = 0;
43194
43304
  for (const element of elements) {
43195
- const result = await executeSplit(pid, element, splitTime, activeCompPath, writeProjectFile);
43305
+ const result = await executeSplit(
43306
+ pid,
43307
+ element,
43308
+ splitTime,
43309
+ activeCompPath,
43310
+ writeProjectFile,
43311
+ observeProjectFileVersion
43312
+ );
43196
43313
  if (!result.changed) continue;
43197
43314
  snapshots.set(result.targetPath, {
43198
43315
  before: getOriginalContent(originals, result.targetPath),
43199
43316
  after: result.patchedContent
43200
43317
  });
43201
- await writeProjectFile(result.targetPath, result.patchedContent);
43318
+ await writeProjectFile(result.targetPath, result.patchedContent, result.patchedContent);
43202
43319
  count++;
43203
43320
  }
43204
43321
  return count;
43205
43322
  }
43206
- async function executeSplit(pid, element, splitTime, activeCompPath, writeProjectFile) {
43323
+ async function executeSplit(pid, element, splitTime, activeCompPath, writeProjectFile, observeProjectFileVersion) {
43207
43324
  const patchTarget = buildPatchTarget(element);
43208
43325
  if (!patchTarget) throw new Error("Clip is missing a patchable target.");
43209
43326
  const targetPath = element.sourceFile || activeCompPath || "index.html";
@@ -43225,6 +43342,7 @@ async function executeSplit(pid, element, splitTime, activeCompPath, writeProjec
43225
43342
  if (!splitResult.changed) {
43226
43343
  return { targetPath, originalContent, patchedContent: originalContent, changed: false };
43227
43344
  }
43345
+ observeProjectFileVersion?.(targetPath, splitResult.version);
43228
43346
  let patchedContent = typeof splitResult.content === "string" ? splitResult.content : originalContent;
43229
43347
  let skippedSelectors;
43230
43348
  if (element.domId) {
@@ -43239,9 +43357,10 @@ async function executeSplit(pid, element, splitTime, activeCompPath, writeProjec
43239
43357
  element.duration
43240
43358
  );
43241
43359
  if (gsapResult.content) patchedContent = gsapResult.content;
43360
+ if (gsapResult.version) observeProjectFileVersion?.(targetPath, gsapResult.version);
43242
43361
  if (gsapResult.skippedSelectors?.length) skippedSelectors = gsapResult.skippedSelectors;
43243
43362
  } catch (gsapError) {
43244
- await writeProjectFile(targetPath, originalContent);
43363
+ await writeProjectFile(targetPath, originalContent, patchedContent);
43245
43364
  throw gsapError;
43246
43365
  }
43247
43366
  }
@@ -43252,6 +43371,7 @@ function useRazorSplit({
43252
43371
  activeCompPath,
43253
43372
  showToast,
43254
43373
  writeProjectFile,
43374
+ observeProjectFileVersion,
43255
43375
  recordEdit,
43256
43376
  domEditSaveTimestampRef,
43257
43377
  reloadPreview,
@@ -43270,7 +43390,14 @@ function useRazorSplit({
43270
43390
  const pid = projectIdRef.current;
43271
43391
  if (!pid || !canSplitElementAt(element, splitTime)) return;
43272
43392
  try {
43273
- const { targetPath, originalContent, patchedContent, changed, skippedSelectors } = await executeSplit(pid, element, splitTime, activeCompPath, writeProjectFile);
43393
+ const { targetPath, originalContent, patchedContent, changed, skippedSelectors } = await executeSplit(
43394
+ pid,
43395
+ element,
43396
+ splitTime,
43397
+ activeCompPath,
43398
+ writeProjectFile,
43399
+ observeProjectFileVersion
43400
+ );
43274
43401
  if (!changed) {
43275
43402
  showToast("Failed to split clip \u2014 playhead may be outside the clip", "error");
43276
43403
  return;
@@ -43305,6 +43432,7 @@ function useRazorSplit({
43305
43432
  recordEdit,
43306
43433
  showToast,
43307
43434
  writeProjectFile,
43435
+ observeProjectFileVersion,
43308
43436
  domEditSaveTimestampRef,
43309
43437
  reloadPreview,
43310
43438
  forceReloadSdkSession,
@@ -43312,6 +43440,7 @@ function useRazorSplit({
43312
43440
  ]
43313
43441
  );
43314
43442
  const handleRazorSplitAll = useCallback68(
43443
+ // fallow-ignore-next-line complexity
43315
43444
  async (splitTime) => {
43316
43445
  if (isRecordingRef?.current) {
43317
43446
  showToast("Cannot edit timeline while recording", "error");
@@ -43333,7 +43462,8 @@ function useRazorSplit({
43333
43462
  activeCompPath,
43334
43463
  originals,
43335
43464
  finalSnapshots,
43336
- writeProjectFile
43465
+ writeProjectFile,
43466
+ observeProjectFileVersion
43337
43467
  );
43338
43468
  if (splitCount === 0) return;
43339
43469
  domEditSaveTimestampRef.current = Date.now();
@@ -43348,7 +43478,7 @@ function useRazorSplit({
43348
43478
  showToast(`Split ${splitCount} clips at ${splitTime.toFixed(2)}s`, "info");
43349
43479
  } catch (error) {
43350
43480
  try {
43351
- await restoreFilesToOriginal(originals, finalSnapshots.keys(), writeProjectFile);
43481
+ await restoreFilesToOriginal(originals, finalSnapshots, writeProjectFile);
43352
43482
  } catch {
43353
43483
  }
43354
43484
  const message = error instanceof Error ? error.message : "Failed to split clips";
@@ -43360,6 +43490,7 @@ function useRazorSplit({
43360
43490
  recordEdit,
43361
43491
  showToast,
43362
43492
  writeProjectFile,
43493
+ observeProjectFileVersion,
43363
43494
  domEditSaveTimestampRef,
43364
43495
  reloadPreview,
43365
43496
  forceReloadSdkSession,
@@ -44059,6 +44190,7 @@ function useTimelineEditing({
44059
44190
  timelineElements,
44060
44191
  showToast,
44061
44192
  writeProjectFile,
44193
+ observeProjectFileVersion,
44062
44194
  recordEdit,
44063
44195
  domEditSaveTimestampRef,
44064
44196
  reloadPreview,
@@ -44440,6 +44572,7 @@ function useTimelineEditing({
44440
44572
  activeCompPath,
44441
44573
  showToast,
44442
44574
  writeProjectFile,
44575
+ observeProjectFileVersion,
44443
44576
  recordEdit,
44444
44577
  domEditSaveTimestampRef,
44445
44578
  reloadPreview,
@@ -46576,7 +46709,7 @@ function useDomEditCommits({
46576
46709
  const preparedContent = options.prepareContent(patchedContent, targetPath);
46577
46710
  if (preparedContent !== patchedContent) {
46578
46711
  try {
46579
- await writeProjectFile(targetPath, preparedContent);
46712
+ await writeProjectFile(targetPath, preparedContent, patchedContent);
46580
46713
  finalContent = preparedContent;
46581
46714
  } catch (error) {
46582
46715
  showToast(
@@ -46854,7 +46987,7 @@ function useGroupCommits(params) {
46854
46987
  }
46855
46988
 
46856
46989
  // src/hooks/useGsapScriptCommits.ts
46857
- import { useCallback as useCallback87, useMemo as useMemo34, useRef as useRef83 } from "react";
46990
+ import { useCallback as useCallback87, useMemo as useMemo35, useRef as useRef83 } from "react";
46858
46991
  import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
46859
46992
 
46860
46993
  // src/hooks/gsapRuntimePatch.ts
@@ -48101,7 +48234,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
48101
48234
  if (!result) return;
48102
48235
  await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, options);
48103
48236
  }, [showToast, finalizeSuccessfulMutation]);
48104
- const commitMutation = useMemo34(() => {
48237
+ const commitMutation = useMemo35(() => {
48105
48238
  const serializeFile = (file, task) => {
48106
48239
  if (writeProjectFile) {
48107
48240
  return serializeStudioFileMutation(writeProjectFile, file, task);
@@ -48166,7 +48299,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
48166
48299
  },
48167
48300
  [activeProjectId]
48168
48301
  );
48169
- const sdkDeps = useMemo34(
48302
+ const sdkDeps = useMemo35(
48170
48303
  () => writeProjectFile ? {
48171
48304
  editHistory: { recordEdit: editHistory.recordEdit },
48172
48305
  writeProjectFile,
@@ -50426,7 +50559,7 @@ function useStudioSdkSessions(projectId, activeCompPath, domEditSaveTimestampRef
50426
50559
  }
50427
50560
 
50428
50561
  // src/hooks/useBlockHandlers.ts
50429
- import { useCallback as useCallback96, useMemo as useMemo35, useRef as useRef89, useState as useState76 } from "react";
50562
+ import { useCallback as useCallback96, useMemo as useMemo36, useRef as useRef89, useState as useState76 } from "react";
50430
50563
 
50431
50564
  // src/utils/blockInstaller.ts
50432
50565
  function getMaxZIndexFromIframe(iframe) {
@@ -50565,7 +50698,7 @@ function useBlockHandlers({
50565
50698
  setRightPanelTab
50566
50699
  }) {
50567
50700
  const [activeBlockParams, setActiveBlockParams] = useState76(null);
50568
- const blockCtx = useMemo35(
50701
+ const blockCtx = useMemo36(
50569
50702
  () => ({
50570
50703
  activeCompPath: blockCtxDeps.activeCompPath,
50571
50704
  timelineElements: blockCtxDeps.timelineElements,
@@ -52255,7 +52388,7 @@ function useFrameCapture({
52255
52388
  }
52256
52389
 
52257
52390
  // src/hooks/useLintModal.ts
52258
- import { useState as useState81, useCallback as useCallback104, useEffect as useEffect71, useRef as useRef96, useMemo as useMemo36 } from "react";
52391
+ import { useState as useState81, useCallback as useCallback104, useEffect as useEffect71, useRef as useRef96, useMemo as useMemo37 } from "react";
52259
52392
  function parseFinding(f) {
52260
52393
  return {
52261
52394
  severity: f.severity === "error" ? "error" : "warning",
@@ -52327,8 +52460,8 @@ function useLintModal(projectId, refreshKey) {
52327
52460
  },
52328
52461
  [backgroundFindings]
52329
52462
  );
52330
- const findingsByElement = useMemo36(() => groupFindings((f) => f.elementId), [groupFindings]);
52331
- const findingsByFile = useMemo36(() => groupFindings((f) => f.file), [groupFindings]);
52463
+ const findingsByElement = useMemo37(() => groupFindings((f) => f.elementId), [groupFindings]);
52464
+ const findingsByFile = useMemo37(() => groupFindings((f) => f.file), [groupFindings]);
52332
52465
  useEffect71(() => {
52333
52466
  usePlayerStore.getState().setLintFindingsByElement(findingsByElement);
52334
52467
  }, [findingsByElement]);
@@ -52687,7 +52820,7 @@ function useStudioUrlState({
52687
52820
  }
52688
52821
 
52689
52822
  // src/hooks/useStudioContextValue.ts
52690
- import { useCallback as useCallback108, useMemo as useMemo37, useRef as useRef99, useState as useState84 } from "react";
52823
+ import { useCallback as useCallback108, useMemo as useMemo38, useRef as useRef99, useState as useState84 } from "react";
52691
52824
  function buildStudioContextValue(input) {
52692
52825
  return {
52693
52826
  projectId: input.projectId,
@@ -52712,7 +52845,7 @@ function buildStudioContextValue(input) {
52712
52845
  };
52713
52846
  }
52714
52847
  function useInspectorState(rightPanelTab, rightInspectorPanes, rightCollapsed, isPlaying, isGestureRecording) {
52715
- return useMemo37(() => {
52848
+ return useMemo38(() => {
52716
52849
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
52717
52850
  const layersPanelActive = STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.layers;
52718
52851
  const designPanelActive = STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.design;
@@ -52774,7 +52907,7 @@ function useGlobalFileDrop(handleTimelineFileDrop) {
52774
52907
  import { useRef as useRef100 } from "react";
52775
52908
 
52776
52909
  // src/contexts/PanelLayoutContext.tsx
52777
- import { createContext as createContext8, useContext as useContext8, useMemo as useMemo38 } from "react";
52910
+ import { createContext as createContext8, useContext as useContext8, useMemo as useMemo39 } from "react";
52778
52911
  import { jsx as jsx114 } from "react/jsx-runtime";
52779
52912
  var PanelLayoutContext = createContext8(null);
52780
52913
  function usePanelLayoutContext() {
@@ -52796,6 +52929,7 @@ function PanelLayoutProvider({
52796
52929
  setRightPanelTab,
52797
52930
  rightInspectorPanes,
52798
52931
  toggleRightInspectorPane,
52932
+ setExclusiveRightInspectorPane,
52799
52933
  toggleLeftSidebar,
52800
52934
  handlePanelResizeStart,
52801
52935
  handlePanelResizeMove,
@@ -52803,7 +52937,7 @@ function PanelLayoutProvider({
52803
52937
  },
52804
52938
  children
52805
52939
  }) {
52806
- const stable = useMemo38(
52940
+ const stable = useMemo39(
52807
52941
  () => ({
52808
52942
  leftWidth,
52809
52943
  setLeftWidth,
@@ -52817,6 +52951,7 @@ function PanelLayoutProvider({
52817
52951
  setRightPanelTab,
52818
52952
  rightInspectorPanes,
52819
52953
  toggleRightInspectorPane,
52954
+ setExclusiveRightInspectorPane,
52820
52955
  toggleLeftSidebar,
52821
52956
  handlePanelResizeStart,
52822
52957
  handlePanelResizeMove,
@@ -52835,6 +52970,7 @@ function PanelLayoutProvider({
52835
52970
  setRightPanelTab,
52836
52971
  rightInspectorPanes,
52837
52972
  toggleRightInspectorPane,
52973
+ setExclusiveRightInspectorPane,
52838
52974
  toggleLeftSidebar,
52839
52975
  handlePanelResizeStart,
52840
52976
  handlePanelResizeMove,
@@ -52850,7 +52986,7 @@ import {
52850
52986
  useCallback as useCallback109,
52851
52987
  useContext as useContext9,
52852
52988
  useEffect as useEffect73,
52853
- useMemo as useMemo39,
52989
+ useMemo as useMemo40,
52854
52990
  useState as useState85
52855
52991
  } from "react";
52856
52992
  import { jsx as jsx115 } from "react/jsx-runtime";
@@ -52880,7 +53016,7 @@ function useViewModeState() {
52880
53016
  setMode(mode);
52881
53017
  writeViewModeToUrl(mode);
52882
53018
  }, []);
52883
- return useMemo39(() => ({ viewMode, setViewMode }), [viewMode, setViewMode]);
53019
+ return useMemo40(() => ({ viewMode, setViewMode }), [viewMode, setViewMode]);
52884
53020
  }
52885
53021
  var ViewModeContext = createContext9(null);
52886
53022
  function useViewMode() {
@@ -54056,7 +54192,7 @@ function useGestureCommit({
54056
54192
  }
54057
54193
 
54058
54194
  // src/components/editor/GestureTrailOverlay.tsx
54059
- import { memo as memo36, useMemo as useMemo40 } from "react";
54195
+ import { memo as memo36, useMemo as useMemo41 } from "react";
54060
54196
  import { Fragment as Fragment36, jsx as jsx117, jsxs as jsxs101 } from "react/jsx-runtime";
54061
54197
  var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54062
54198
  samples,
@@ -54068,7 +54204,7 @@ var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54068
54204
  mode,
54069
54205
  accentColor = "#3CE6AC"
54070
54206
  }) {
54071
- const trailPoints = useMemo40(() => {
54207
+ const trailPoints = useMemo41(() => {
54072
54208
  if (!canvasRect) return "";
54073
54209
  if (trail && trail.length > 1) {
54074
54210
  return trail.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
@@ -54076,7 +54212,7 @@ var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54076
54212
  if (samples.length === 0) return "";
54077
54213
  return samples.filter((s) => s.properties.x != null && s.properties.y != null).map((s) => `${s.properties.x},${s.properties.y}`).join(" ");
54078
54214
  }, [samples, trail, sampleCount, canvasRect?.left, canvasRect?.top]);
54079
- const simplifiedPath = useMemo40(() => {
54215
+ const simplifiedPath = useMemo41(() => {
54080
54216
  if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
54081
54217
  const pts = [];
54082
54218
  for (const [pct, props] of simplifiedPoints) {
@@ -54088,7 +54224,7 @@ var GestureTrailOverlay = memo36(function GestureTrailOverlay2({
54088
54224
  if (pts.length === 0) return "";
54089
54225
  return pts.map((p) => `${p.x},${p.y}`).join(" ");
54090
54226
  }, [simplifiedPoints]);
54091
- const diamondPositions = useMemo40(() => {
54227
+ const diamondPositions = useMemo41(() => {
54092
54228
  if (!simplifiedPoints || simplifiedPoints.size === 0) return [];
54093
54229
  const pts = [];
54094
54230
  for (const [pct, props] of simplifiedPoints) {
@@ -54411,7 +54547,7 @@ var CompositionsTab = memo37(function CompositionsTab2({
54411
54547
  });
54412
54548
 
54413
54549
  // 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";
54550
+ import { memo as memo38, useState as useState93, useCallback as useCallback115, useRef as useRef106, useMemo as useMemo43, useEffect as useEffect81 } from "react";
54415
54551
 
54416
54552
  // src/components/sidebar/assetHelpers.ts
54417
54553
  function getCategory(path) {
@@ -54781,7 +54917,7 @@ function AudioRow({
54781
54917
  }
54782
54918
 
54783
54919
  // src/components/sidebar/GlobalAssetsView.tsx
54784
- import { useEffect as useEffect78, useMemo as useMemo41, useState as useState90 } from "react";
54920
+ import { useEffect as useEffect78, useMemo as useMemo42, useState as useState90 } from "react";
54785
54921
  import { jsx as jsx121, jsxs as jsxs105 } from "react/jsx-runtime";
54786
54922
  function globalAssetRows(records, query = "") {
54787
54923
  const q = query.trim().toLowerCase();
@@ -54808,7 +54944,7 @@ function GlobalAssetsView({ searchQuery }) {
54808
54944
  cancelled = true;
54809
54945
  };
54810
54946
  }, []);
54811
- const rows = useMemo41(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
54947
+ const rows = useMemo42(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
54812
54948
  if (records === null) {
54813
54949
  return /* @__PURE__ */ jsx121("p", { className: "px-4 py-3 text-[11px] text-panel-text-5", children: "Loading global assets\u2026" });
54814
54950
  }
@@ -55220,8 +55356,8 @@ var AssetsTab = memo38(function AssetsTab2({
55220
55356
  }
55221
55357
  }, []);
55222
55358
  const elements = usePlayerStore((s) => s.elements);
55223
- const usedPaths = useMemo42(() => deriveUsedPaths(elements), [elements]);
55224
- const mediaAssets = useMemo42(() => {
55359
+ const usedPaths = useMemo43(() => deriveUsedPaths(elements), [elements]);
55360
+ const mediaAssets = useMemo43(() => {
55225
55361
  const media = assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a));
55226
55362
  const all = filterByUsage(media, usedPaths, usageFilter);
55227
55363
  if (!searchQuery) return all;
@@ -55233,7 +55369,7 @@ var AssetsTab = memo38(function AssetsTab2({
55233
55369
  return rec?.description?.toLowerCase().includes(q);
55234
55370
  });
55235
55371
  }, [assets, searchQuery, manifest, usageFilter, usedPaths]);
55236
- const categorized = useMemo42(() => {
55372
+ const categorized = useMemo43(() => {
55237
55373
  const groups = { audio: [], images: [], video: [], fonts: [] };
55238
55374
  for (const a of mediaAssets) {
55239
55375
  const cat = getCategory(a);
@@ -55248,12 +55384,12 @@ var AssetsTab = memo38(function AssetsTab2({
55248
55384
  }
55249
55385
  return groups;
55250
55386
  }, [mediaAssets, usedPaths]);
55251
- const counts = useMemo42(() => {
55387
+ const counts = useMemo43(() => {
55252
55388
  const c = { all: mediaAssets.length };
55253
55389
  for (const cat of FILTER_ORDER) c[cat] = categorized[cat].length;
55254
55390
  return c;
55255
55391
  }, [mediaAssets, categorized]);
55256
- const usageCounts = useMemo42(
55392
+ const usageCounts = useMemo43(
55257
55393
  () => countUsage(
55258
55394
  assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
55259
55395
  usedPaths
@@ -55500,7 +55636,7 @@ import { memo as memo39, useState as useState95, useCallback as useCallback116,
55500
55636
  import { createPortal as createPortal7 } from "react-dom";
55501
55637
 
55502
55638
  // src/hooks/useBlockCatalog.ts
55503
- import { useState as useState94, useEffect as useEffect82, useMemo as useMemo43 } from "react";
55639
+ import { useState as useState94, useEffect as useEffect82, useMemo as useMemo44 } from "react";
55504
55640
 
55505
55641
  // src/utils/blockCategories.ts
55506
55642
  import {
@@ -55554,7 +55690,7 @@ function useBlockCatalog() {
55554
55690
  cancelled = true;
55555
55691
  };
55556
55692
  }, []);
55557
- const filteredBlocks = useMemo43(() => {
55693
+ const filteredBlocks = useMemo44(() => {
55558
55694
  let result = blocks;
55559
55695
  if (category) {
55560
55696
  result = result.filter((b) => b.category === category);
@@ -56564,7 +56700,7 @@ function StudioLeftSidebar({
56564
56700
  }
56565
56701
 
56566
56702
  // src/components/StudioRightPanel.tsx
56567
- import { useCallback as useCallback134, useEffect as useEffect89, useMemo as useMemo45, useRef as useRef114 } from "react";
56703
+ import { useCallback as useCallback134, useEffect as useEffect89, useMemo as useMemo46, useRef as useRef114 } from "react";
56568
56704
 
56569
56705
  // src/components/editor/LayersPanel.tsx
56570
56706
  import { memo as memo41, useState as useState99, useCallback as useCallback120, useEffect as useEffect84, useRef as useRef110 } from "react";
@@ -59241,7 +59377,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
59241
59377
  }
59242
59378
 
59243
59379
  // src/components/panels/VariablesPanel.tsx
59244
- import { memo as memo47, useCallback as useCallback130, useEffect as useEffect88, useMemo as useMemo44, useState as useState109 } from "react";
59380
+ import { memo as memo47, useCallback as useCallback130, useEffect as useEffect88, useMemo as useMemo45, useState as useState109 } from "react";
59245
59381
 
59246
59382
  // src/hooks/useVariablesPersist.ts
59247
59383
  import { useCallback as useCallback127 } from "react";
@@ -59945,22 +60081,22 @@ var VariablesPanel = memo47(function VariablesPanel2({
59945
60081
  domEditSaveTimestampRef,
59946
60082
  publishSdkSession
59947
60083
  });
59948
- const declarations = useMemo44(
60084
+ const declarations = useMemo45(
59949
60085
  () => sdkSession?.getVariableDeclarations() ?? [],
59950
60086
  // eslint-disable-next-line react-hooks/exhaustive-deps
59951
60087
  [sdkSession, refreshKey, revision]
59952
60088
  );
59953
- const usage = useMemo44(
60089
+ const usage = useMemo45(
59954
60090
  () => sdkSession?.getVariableUsage() ?? null,
59955
60091
  // eslint-disable-next-line react-hooks/exhaustive-deps
59956
60092
  [sdkSession, refreshKey, revision]
59957
60093
  );
59958
- const issues = useMemo44(
60094
+ const issues = useMemo45(
59959
60095
  () => previewValues && sdkSession ? sdkSession.validateVariableValues(previewValues) : [],
59960
60096
  // eslint-disable-next-line react-hooks/exhaustive-deps
59961
60097
  [sdkSession, previewValues, refreshKey, revision]
59962
60098
  );
59963
- const effectiveValues = useMemo44(
60099
+ const effectiveValues = useMemo45(
59964
60100
  () => sdkSession?.getVariableValues(previewValues ?? void 0) ?? {},
59965
60101
  // eslint-disable-next-line react-hooks/exhaustive-deps
59966
60102
  [sdkSession, previewValues, refreshKey, revision]
@@ -60617,6 +60753,7 @@ function StudioRightPanel({
60617
60753
  setRightPanelTab,
60618
60754
  rightInspectorPanes,
60619
60755
  toggleRightInspectorPane,
60756
+ setExclusiveRightInspectorPane,
60620
60757
  handlePanelResizeStart,
60621
60758
  handlePanelResizeMove,
60622
60759
  handlePanelResizeEnd
@@ -60722,7 +60859,7 @@ function StudioRightPanel({
60722
60859
  );
60723
60860
  const renderJobs = renderQueue.jobs;
60724
60861
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
60725
- const slideshowScenes = useMemo45(() => {
60862
+ const slideshowScenes = useMemo46(() => {
60726
60863
  try {
60727
60864
  const win = previewIframeRef.current?.contentWindow;
60728
60865
  return (win?.__clipManifest?.scenes ?? []).map((s) => ({
@@ -60742,6 +60879,10 @@ function StudioRightPanel({
60742
60879
  setRightPanelTab(pane);
60743
60880
  return;
60744
60881
  }
60882
+ if (STUDIO_FLAT_INSPECTOR_ENABLED) {
60883
+ setExclusiveRightInspectorPane(pane);
60884
+ return;
60885
+ }
60745
60886
  toggleRightInspectorPane(pane);
60746
60887
  };
60747
60888
  const handleApplyColorGradingScope = useCallback134(
@@ -61033,7 +61174,7 @@ function StudioRightPanel({
61033
61174
  domEditSaveTimestampRef,
61034
61175
  recordEdit
61035
61176
  }
61036
- ) : layersPaneOpen && designPaneOpen ? /* @__PURE__ */ jsxs124("div", { ref: splitContainerRef, className: "flex h-full min-h-0 min-w-0 flex-col", children: [
61177
+ ) : 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
61178
  /* @__PURE__ */ jsx144(
61038
61179
  "div",
61039
61180
  {
@@ -61856,7 +61997,7 @@ function useProjectSignaturePoll(projectId, currentSignature, onChange) {
61856
61997
  }
61857
61998
 
61858
61999
  // src/components/storyboard/StoryboardLoaded.tsx
61859
- import { useEffect as useEffect98, useMemo as useMemo48, useState as useState116 } from "react";
62000
+ import { useEffect as useEffect98, useMemo as useMemo49, useState as useState116 } from "react";
61860
62001
 
61861
62002
  // src/components/storyboard/StoryboardDirection.tsx
61862
62003
  import { jsx as jsx146, jsxs as jsxs126 } from "react/jsx-runtime";
@@ -62106,7 +62247,7 @@ function StoryboardScriptPanel({ script }) {
62106
62247
  }
62107
62248
 
62108
62249
  // src/components/storyboard/StoryboardSourceEditor.tsx
62109
- import { useCallback as useCallback138, useEffect as useEffect95, useMemo as useMemo46, useRef as useRef118, useState as useState113 } from "react";
62250
+ import { useCallback as useCallback138, useEffect as useEffect95, useMemo as useMemo47, useRef as useRef118, useState as useState113 } from "react";
62110
62251
  import { marked } from "marked";
62111
62252
  import DOMPurify from "dompurify";
62112
62253
  import { jsx as jsx152, jsxs as jsxs130 } from "react/jsx-runtime";
@@ -62165,7 +62306,7 @@ function useMarkdownPreview(source) {
62165
62306
  const id = window.setTimeout(() => setDebounced(source), 200);
62166
62307
  return () => window.clearTimeout(id);
62167
62308
  }, [source]);
62168
- return useMemo46(() => {
62309
+ return useMemo47(() => {
62169
62310
  const html2 = marked.parse(debounced, { async: false });
62170
62311
  DOMPurify.addHook("afterSanitizeAttributes", hardenLinks);
62171
62312
  try {
@@ -62484,7 +62625,7 @@ function StatusRow({
62484
62625
  }
62485
62626
 
62486
62627
  // src/components/storyboard/useFrameComments.ts
62487
- import { useCallback as useCallback140, useEffect as useEffect97, useMemo as useMemo47, useState as useState115 } from "react";
62628
+ import { useCallback as useCallback140, useEffect as useEffect97, useMemo as useMemo48, useState as useState115 } from "react";
62488
62629
 
62489
62630
  // src/components/storyboard/frameComments.ts
62490
62631
  var FRAME_COMMENTS_PATH = ".hyperframes/frame-comments.json";
@@ -62565,7 +62706,7 @@ function useFrameComments(frames) {
62565
62706
  const setDraft = useCallback140((index, text) => {
62566
62707
  setDrafts((prev) => ({ ...prev, [index]: text }));
62567
62708
  }, []);
62568
- const draftCount = useMemo47(
62709
+ const draftCount = useMemo48(
62569
62710
  () => Object.values(drafts).filter((text) => text.trim().length > 0).length,
62570
62711
  [drafts]
62571
62712
  );
@@ -62606,7 +62747,7 @@ function StoryboardLoaded({
62606
62747
  useEffect98(() => {
62607
62748
  void refreshPending();
62608
62749
  }, [data.signature, refreshPending]);
62609
- const sourceFiles = useMemo48(() => {
62750
+ const sourceFiles = useMemo49(() => {
62610
62751
  const files = [{ path: data.path, label: data.path }];
62611
62752
  if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path });
62612
62753
  return files;
@@ -62958,7 +63099,7 @@ function StudioApp() {
62958
63099
  const isPlaying = usePlayerStore((s) => s.isPlaying);
62959
63100
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
62960
63101
  const activePreviewUrl = activeCompPath ? `/api/projects/${projectId}/preview/comp/${activeCompPath}` : null;
62961
- const effectiveTimelineDuration = useMemo49(() => {
63102
+ const effectiveTimelineDuration = useMemo50(() => {
62962
63103
  const maxEnd = timelineElements.length > 0 ? Math.max(...timelineElements.map((el) => el.start + el.duration)) : 0;
62963
63104
  return Math.max(timelineDuration, maxEnd);
62964
63105
  }, [timelineDuration, timelineElements]);
@@ -62980,7 +63121,7 @@ function StudioApp() {
62980
63121
  domEditSaveTimestampRef,
62981
63122
  setRefreshKey
62982
63123
  });
62983
- const masterCompPath = useMemo49(
63124
+ const masterCompPath = useMemo50(
62984
63125
  () => resolveMasterCompositionPath(fileManager.fileTree),
62985
63126
  [fileManager.fileTree]
62986
63127
  );
@@ -63018,6 +63159,7 @@ function StudioApp() {
63018
63159
  timelineElements,
63019
63160
  showToast,
63020
63161
  writeProjectFile: fileManager.writeProjectFile,
63162
+ observeProjectFileVersion: fileManager.observeProjectFileVersion,
63021
63163
  recordEdit: editHistory.recordEdit,
63022
63164
  domEditSaveTimestampRef,
63023
63165
  reloadPreview,
@@ -63296,7 +63438,7 @@ function StudioApp() {
63296
63438
  handlePreviewIframeRef,
63297
63439
  refreshPreviewDocumentVersion
63298
63440
  });
63299
- const timelineToolbar = useMemo49(
63441
+ const timelineToolbar = useMemo50(
63300
63442
  () => /* @__PURE__ */ jsx157(
63301
63443
  TimelineToolbar,
63302
63444
  {