@hyperframes/studio 0.8.81 → 0.8.82

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 (98) hide show
  1. package/dist/assets/{hyperframes-player-C68UawR3.js → hyperframes-player-zGIbZhLJ.js} +1 -1
  2. package/dist/assets/{index-CMzPGv9P.js → index-BiLCTunu.js} +1 -1
  3. package/dist/assets/{index-hNVM9Pd2.js → index-DSazIt7R.js} +189 -189
  4. package/dist/assets/{index-CS5SImRT.js → index-DStmfnpZ.js} +1 -1
  5. package/dist/index.d.ts +172 -7
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +2414 -2172
  8. package/dist/index.js.map +1 -1
  9. package/package.json +8 -8
  10. package/src/components/TimelineHistoryButtons.dom.test.tsx +38 -1
  11. package/src/components/TimelineHistoryButtons.tsx +26 -14
  12. package/src/components/TimelineToolbar.test.tsx +9 -1
  13. package/src/components/TimelineToolbar.tsx +13 -5
  14. package/src/components/editor/BlockParamsPanel.tsx +14 -10
  15. package/src/components/editor/anchoredResizeReleaseShift.test.ts +2 -6
  16. package/src/components/editor/manualEdits.test.ts +24 -0
  17. package/src/components/editor/manualEdits.ts +6 -2
  18. package/src/components/editor/manualEditsDom.ts +0 -65
  19. package/src/components/editor/manualEditsSeekReapply.ts +136 -0
  20. package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +71 -32
  21. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +62 -0
  22. package/src/components/editor/propertyPanelFlatMediaSection.tsx +4 -6
  23. package/src/components/editor/propertyPanelHelpers.ts +15 -0
  24. package/src/components/editor/propertyPanelMediaSection.tsx +3 -5
  25. package/src/components/editor/reapplyBoxSizeAfterSeek.test.ts +1 -1
  26. package/src/components/editor/reapplyPositionEditsAfterSeek.test.ts +117 -0
  27. package/src/components/editor/useFxLevelling.ts +35 -10
  28. package/src/components/nle/AudioMeterStrip.test.tsx +26 -3
  29. package/src/components/nle/AudioMeterStrip.tsx +23 -12
  30. package/src/components/pointerTargetSize.test.tsx +1 -1
  31. package/src/components/studioColorGradingScope.ts +12 -36
  32. package/src/contexts/TimelineEditContext.test.tsx +49 -0
  33. package/src/contexts/TimelineEditContext.tsx +27 -23
  34. package/src/hooks/canvasQuickSaves.test.tsx +195 -0
  35. package/src/hooks/gsapMutationClient.ts +116 -0
  36. package/src/hooks/resizeObserverTestUtils.ts +14 -0
  37. package/src/hooks/timelineAudioGroupCreate.ts +18 -40
  38. package/src/hooks/timelineEditingHelpers.test.ts +21 -0
  39. package/src/hooks/timelineEditingHelpers.ts +66 -61
  40. package/src/hooks/timelineQuickSaves.test.ts +195 -0
  41. package/src/hooks/timelineTimingSync.test.ts +12 -0
  42. package/src/hooks/timelineTimingSync.ts +31 -123
  43. package/src/hooks/timelineTrackVisibility.ts +15 -29
  44. package/src/hooks/useAskAgentModal.test.tsx +78 -0
  45. package/src/hooks/useAskAgentModal.ts +3 -1
  46. package/src/hooks/useClipboard.source.test.ts +32 -0
  47. package/src/hooks/useClipboard.ts +21 -20
  48. package/src/hooks/useDomEditCommits.test.tsx +13 -9
  49. package/src/hooks/useDomEditCommits.ts +88 -97
  50. package/src/hooks/useDomEditCommitsHelpers.ts +69 -6
  51. package/src/hooks/useEditorSave.ts +1 -1
  52. package/src/hooks/useElementLifecycleOps.ts +36 -37
  53. package/src/hooks/useFileManager.projectOwnership.test.tsx +51 -4
  54. package/src/hooks/useFileManager.ts +21 -12
  55. package/src/hooks/useGroupCommits.ts +23 -24
  56. package/src/hooks/useGsapAnimationOps.ts +26 -8
  57. package/src/hooks/useGsapScriptCommits.ts +1 -0
  58. package/src/hooks/useThumbnailStripSize.test.tsx +47 -0
  59. package/src/hooks/useThumbnailStripSize.ts +31 -0
  60. package/src/hooks/useTimelineAssetDropOps.ts +38 -35
  61. package/src/hooks/useTimelineDeleteOps.ts +53 -56
  62. package/src/hooks/useTimelineEditing.ts +3 -1
  63. package/src/hooks/useTimelineGroupEditing.ts +5 -1
  64. package/src/index.ts +8 -0
  65. package/src/player/components/CompositionThumbnail.test.ts +40 -6
  66. package/src/player/components/CompositionThumbnail.tsx +6 -21
  67. package/src/player/components/ImageThumbnail.test.tsx +27 -6
  68. package/src/player/components/ImageThumbnail.tsx +4 -18
  69. package/src/player/components/PlayerControls.test.tsx +52 -0
  70. package/src/player/components/PlayerControls.tsx +2 -1
  71. package/src/player/components/Timeline.test.ts +28 -3
  72. package/src/player/components/TimelineCanvas.tsx +5 -3
  73. package/src/player/components/VideoThumbnail.test.tsx +33 -11
  74. package/src/player/components/VideoThumbnail.tsx +7 -29
  75. package/src/player/components/thumbnailUtils.test.ts +16 -20
  76. package/src/player/components/thumbnailUtils.ts +6 -7
  77. package/src/player/components/timelineEditing.test.ts +55 -0
  78. package/src/player/components/timelineGroupEditing.ts +13 -6
  79. package/src/player/components/timelinePlayheadTransform.test.ts +33 -0
  80. package/src/player/components/timelinePlayheadTransform.ts +17 -0
  81. package/src/player/components/useTimelinePlayhead.ts +26 -8
  82. package/src/player/hooks/timelinePlayerSync.test.ts +20 -0
  83. package/src/player/hooks/timelinePlayerSync.ts +18 -8
  84. package/src/player/hooks/useShadowPreviewReload.ts +15 -4
  85. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +19 -0
  86. package/src/player/hooks/useTimelinePlayer.test.ts +93 -1
  87. package/src/player/lib/playbackShortcuts.ts +18 -0
  88. package/src/player/lib/playbackTypes.ts +1 -0
  89. package/src/player/lib/timelineDOM.test.ts +92 -0
  90. package/src/player/lib/timelineDOM.ts +0 -2
  91. package/src/player/lib/timelineElementHelpers.ts +47 -10
  92. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  93. package/src/player/store/timelineElement.ts +2 -1
  94. package/src/timelineChromeExports.test.tsx +79 -0
  95. package/src/utils/authoredSource.ts +15 -0
  96. package/src/utils/blockInstaller.ts +32 -27
  97. package/src/utils/studioFileHistory.test.ts +14 -14
  98. package/src/utils/studioFileHistory.ts +31 -2
@@ -1,4 +1,8 @@
1
1
  import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids";
2
+ import {
3
+ STUDIO_PREVIEW_LAZY_ATTR,
4
+ STUDIO_PREVIEW_UPCOMING_ATTR,
5
+ } from "@hyperframes/core/studio-preview-mark";
2
6
 
3
7
  // Stamped as the preview stamps the files it serves, so a live element's hf-id finds its source.
4
8
  export function parseSavedSource(html: string): Document {
@@ -90,3 +94,14 @@ export function authoredMarkup(authored: Element, live: Element, sourceFile: str
90
94
  }
91
95
  return copy.outerHTML;
92
96
  }
97
+
98
+ export function liveMarkupWithoutPreviewMarks(live: Element): string {
99
+ const copy = live.cloneNode(true) as Element;
100
+ for (const el of [copy, ...Array.from(copy.querySelectorAll("*"))]) {
101
+ el.removeAttribute(STUDIO_PREVIEW_UPCOMING_ATTR);
102
+ if (!el.hasAttribute(STUDIO_PREVIEW_LAZY_ATTR)) continue;
103
+ el.removeAttribute("loading");
104
+ el.removeAttribute(STUDIO_PREVIEW_LAZY_ATTR);
105
+ }
106
+ return copy.outerHTML;
107
+ }
@@ -9,6 +9,7 @@ import { collectHtmlIds } from "./studioHelpers";
9
9
  import { generateId } from "./generateId";
10
10
  import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
11
11
  import { saveProjectFilesWithHistory } from "./studioFileHistory";
12
+ import { serializeStudioFileMutation } from "./studioFileMutationCoordinator";
12
13
  import { extendRootDurationInSource } from "./rootDuration";
13
14
  import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
14
15
 
@@ -93,12 +94,14 @@ async function makeComponentBackgroundTransparent(
93
94
  writeProjectFile: AddBlockOptions["writeProjectFile"],
94
95
  ): Promise<void> {
95
96
  if (block.type !== "hyperframes:component") return;
96
- const content = await readProjectFile(compositionFile);
97
- const transparentContent = content.replace(
98
- /background:\s*(?:#(?:0a0a0a|000000|000|0a0805)|rgba?\([^)]*\))\s*;/g,
99
- "background: transparent;",
100
- );
101
- if (transparentContent !== content) await writeProjectFile(compositionFile, transparentContent);
97
+ await serializeStudioFileMutation(writeProjectFile, compositionFile, async () => {
98
+ const content = await readProjectFile(compositionFile);
99
+ const transparentContent = content.replace(
100
+ /background:\s*(?:#(?:0a0a0a|000000|000|0a0805)|rgba?\([^)]*\))\s*;/g,
101
+ "background: transparent;",
102
+ );
103
+ if (transparentContent !== content) await writeProjectFile(compositionFile, transparentContent);
104
+ });
102
105
  }
103
106
 
104
107
  function resolveBlockPlacement({
@@ -201,7 +204,6 @@ export async function addBlockToProject(
201
204
  );
202
205
 
203
206
  const targetPath = activeCompPath || "index.html";
204
- const originalContent = await readProjectFile(targetPath);
205
207
  const relevantElements = timelineElements.filter(
206
208
  (element) => (element.sourceFile || targetPath) === targetPath,
207
209
  );
@@ -211,29 +213,32 @@ export async function addBlockToProject(
211
213
  timelineElements: relevantElements,
212
214
  currentTime: opts.currentTime ?? 0,
213
215
  });
214
- const { width, height } = resolveTimelineAssetCompositionSize(originalContent);
215
- const hostId = buildUniqueCompositionId(block.name, collectHtmlIds(originalContent));
216
- const subComposition = buildSubCompositionHtml({
217
- id: hostId,
218
- compositionFile,
219
- start,
220
- duration,
221
- track,
222
- width,
223
- height,
224
- left: visualPosition ? Math.round(visualPosition.left) : 0,
225
- top: visualPosition ? Math.round(visualPosition.top) : 0,
226
- zIndex: getMaxZIndexFromIframe(opts.previewIframe ?? null) + 1,
227
- });
228
- const patchedContent = extendRootDurationInSource(
229
- insertTimelineAssetIntoSource(originalContent, subComposition),
230
- start + duration,
231
- );
216
+ let hostId = "";
217
+ const insertHost = (originalContent: string) => {
218
+ const { width, height } = resolveTimelineAssetCompositionSize(originalContent);
219
+ hostId = buildUniqueCompositionId(block.name, collectHtmlIds(originalContent));
220
+ const subComposition = buildSubCompositionHtml({
221
+ id: hostId,
222
+ compositionFile,
223
+ start,
224
+ duration,
225
+ track,
226
+ width,
227
+ height,
228
+ left: visualPosition ? Math.round(visualPosition.left) : 0,
229
+ top: visualPosition ? Math.round(visualPosition.top) : 0,
230
+ zIndex: getMaxZIndexFromIframe(opts.previewIframe ?? null) + 1,
231
+ });
232
+ return extendRootDurationInSource(
233
+ insertTimelineAssetIntoSource(originalContent, subComposition),
234
+ start + duration,
235
+ );
236
+ };
232
237
  await saveProjectFilesWithHistory({
233
238
  projectId,
234
239
  label: `Add ${isBlock ? "block" : "component"}: ${block.title}`,
235
- files: { [targetPath]: patchedContent },
236
- readFile: async () => originalContent,
240
+ files: { [targetPath]: insertHost },
241
+ readFile: readProjectFile,
237
242
  writeFile: writeProjectFile,
238
243
  recordEdit,
239
244
  });
@@ -11,7 +11,7 @@ describe("saveProjectFilesWithHistory", () => {
11
11
  await saveProjectFilesWithHistory({
12
12
  projectId: "project-1",
13
13
  label: "Move layer",
14
- files: { "index.html": "after" },
14
+ files: { "index.html": () => "after" },
15
15
  readFile: async (path) => reads[path],
16
16
  writeFile: async (path, content) => {
17
17
  writes[path] = content;
@@ -40,7 +40,7 @@ describe("saveProjectFilesWithHistory", () => {
40
40
  await saveProjectFilesWithHistory({
41
41
  projectId: "project-1",
42
42
  label: "Delete timeline clip",
43
- files: { "index.html": "removed+shrunk" },
43
+ files: { "index.html": () => "removed+shrunk" },
44
44
  readFile: async () => "original",
45
45
  diskContent: { "index.html": "removed" },
46
46
  writeFile: async (path, _content, expectedContent) => {
@@ -64,7 +64,7 @@ describe("saveProjectFilesWithHistory", () => {
64
64
  await saveProjectFilesWithHistory({
65
65
  projectId: "project-1",
66
66
  label: "Move layer",
67
- files: { "index.html": "after" },
67
+ files: { "index.html": () => "after" },
68
68
  readFile: async () => "before",
69
69
  writeFile: async (path, _content, expectedContent) => {
70
70
  expectations[path] = expectedContent;
@@ -81,7 +81,7 @@ describe("saveProjectFilesWithHistory", () => {
81
81
  const changedPaths = await saveProjectFilesWithHistory({
82
82
  projectId: "project-1",
83
83
  label: "Edit layer",
84
- files: { "index.html": "same" },
84
+ files: { "index.html": () => "same" },
85
85
  readFile: async () => "same",
86
86
  writeFile,
87
87
  recordEdit,
@@ -105,8 +105,8 @@ describe("saveProjectFilesWithHistory", () => {
105
105
  projectId: "project-1",
106
106
  label: "Move layer",
107
107
  files: {
108
- "index.html": "index-after",
109
- "scene.html": "scene-after",
108
+ "index.html": () => "index-after",
109
+ "scene.html": () => "scene-after",
110
110
  },
111
111
  readFile: async (path) => reads[path],
112
112
  writeFile: async (path, content) => {
@@ -139,8 +139,8 @@ describe("saveProjectFilesWithHistory", () => {
139
139
  projectId: "project-1",
140
140
  label: "Move layer",
141
141
  files: {
142
- "index.html": "index-after",
143
- "scene.html": "scene-after",
142
+ "index.html": () => "index-after",
143
+ "scene.html": () => "scene-after",
144
144
  },
145
145
  readFile: async (path) => reads[path],
146
146
  writeFile: async (path, content) => {
@@ -172,8 +172,8 @@ describe("saveProjectFilesWithHistory", () => {
172
172
  projectId: "project-1",
173
173
  label: "Move layer",
174
174
  files: {
175
- "index.html": "index-after",
176
- "scene.html": "scene-after",
175
+ "index.html": () => "index-after",
176
+ "scene.html": () => "scene-after",
177
177
  },
178
178
  readFile: async (path) => reads[path],
179
179
  writeFile: async (path, content) => {
@@ -196,7 +196,7 @@ describe("saveProjectFilesWithHistory", () => {
196
196
  ]);
197
197
  });
198
198
 
199
- it("reads and writes after an earlier same-file mutation completes", async () => {
199
+ it("builds from what the file holds after an earlier same-file mutation completes", async () => {
200
200
  let disk = "before";
201
201
  let release!: () => void;
202
202
  const blocked = new Promise<void>((resolve) => {
@@ -215,7 +215,7 @@ describe("saveProjectFilesWithHistory", () => {
215
215
  const save = saveProjectFilesWithHistory({
216
216
  projectId: "project-1",
217
217
  label: "Edit source",
218
- files: { "index.html": "editor-after" },
218
+ files: { "index.html": (current) => `${current}+editor` },
219
219
  readFile,
220
220
  writeFile,
221
221
  recordEdit,
@@ -227,10 +227,10 @@ describe("saveProjectFilesWithHistory", () => {
227
227
  await priorMutation;
228
228
  await save;
229
229
 
230
- expect(disk).toBe("editor-after");
230
+ expect(disk).toBe("sdk-after+editor");
231
231
  expect(recordEdit).toHaveBeenCalledWith(
232
232
  expect.objectContaining({
233
- files: { "index.html": { before: "sdk-after", after: "editor-after" } },
233
+ files: { "index.html": { before: "sdk-after", after: "sdk-after+editor" } },
234
234
  }),
235
235
  );
236
236
  });
@@ -27,7 +27,7 @@ interface SaveProjectFilesWithHistoryInput {
27
27
  label: string;
28
28
  coalesceKey?: string;
29
29
  coalesceMs?: number;
30
- files: Record<string, string>;
30
+ files: Record<string, (contentInsideFileQueue: string) => string>;
31
31
  readFile: (path: string) => Promise<string>;
32
32
  writeFile: ProjectFileWriter;
33
33
  recordEdit: (entry: RecordEditInput) => Promise<void>;
@@ -68,6 +68,34 @@ export async function saveProjectFilesWithHistory(
68
68
  );
69
69
  }
70
70
 
71
+ /**
72
+ * A server-side rewrite and its undo entry, holding the file's queue from the read to the history write.
73
+ * `rewrite` returns what the server left on disk (and the edit's final content if it goes further), or null.
74
+ */
75
+ export async function saveServerRewriteWithHistory(input: {
76
+ projectId: string;
77
+ path: string;
78
+ label: string;
79
+ coalesceKey?: string;
80
+ writeFile: ProjectFileWriter;
81
+ recordEdit: (entry: RecordEditInput) => Promise<void>;
82
+ rewrite: (original: string) => Promise<{ disk: string; after?: string } | null>;
83
+ }): Promise<boolean> {
84
+ const { projectId, path, writeFile } = input;
85
+ return serializeStudioFileMutations(writeFile, [path], async () => {
86
+ const original = await readProjectFileContent(projectId, path);
87
+ const result = await input.rewrite(original);
88
+ if (!result) return false;
89
+ await writeProjectFilesWithHistoryInQueue({
90
+ ...input,
91
+ files: { [path]: () => result.after ?? result.disk },
92
+ readFile: async () => original,
93
+ diskContent: { [path]: result.disk },
94
+ });
95
+ return true;
96
+ });
97
+ }
98
+
71
99
  export async function writeProjectFilesWithHistoryInQueue({
72
100
  label,
73
101
  coalesceKey,
@@ -79,8 +107,9 @@ export async function writeProjectFilesWithHistoryInQueue({
79
107
  diskContent,
80
108
  }: SaveProjectFilesWithHistoryInput): Promise<string[]> {
81
109
  const snapshots: Record<string, { before: string; after: string }> = {};
82
- for (const [path, after] of Object.entries(files)) {
110
+ for (const [path, build] of Object.entries(files)) {
83
111
  const before = await readFile(path);
112
+ const after = build(before);
84
113
  if (before !== after) {
85
114
  snapshots[path] = { before, after };
86
115
  }