@hyperframes/studio 0.7.70 → 0.7.72

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 (62) hide show
  1. package/dist/assets/{hyperframes-player-DpKeYLEo.js → hyperframes-player-Bjf2HPzR.js} +1 -1
  2. package/dist/assets/index-BgZMle9I.css +1 -0
  3. package/dist/assets/{index-DXjAllII.js → index-Ch1hbJ3e.js} +1 -1
  4. package/dist/assets/{index-CbX-xoud.js → index-Ct_pxETK.js} +1 -1
  5. package/dist/assets/index-DCyWLHnx.js +428 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +5383 -3804
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +4 -4
  12. package/src/components/StudioLeftSidebar.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +10 -4
  14. package/src/components/editor/AnimationCard.test.tsx +100 -0
  15. package/src/components/editor/AnimationCard.tsx +5 -2
  16. package/src/components/editor/EaseCurveSection.test.tsx +348 -0
  17. package/src/components/editor/EaseCurveSection.tsx +444 -237
  18. package/src/components/editor/EaseParamFields.test.tsx +188 -0
  19. package/src/components/editor/EaseParamFields.tsx +246 -0
  20. package/src/components/editor/PropertyPanel.test.tsx +9 -7
  21. package/src/components/editor/PropertyPanelFlat.tsx +74 -88
  22. package/src/components/editor/colorGradingScopePatch.test.ts +3 -3
  23. package/src/components/editor/easeCurveSvg.tsx +65 -0
  24. package/src/components/editor/easePresetLibrary.test.ts +114 -0
  25. package/src/components/editor/easePresetLibrary.ts +49 -0
  26. package/src/components/editor/gsapAnimationConstants.ts +15 -13
  27. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +73 -40
  28. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +85 -66
  29. package/src/components/editor/propertyPanelFlatEffectControl.tsx +96 -0
  30. package/src/components/editor/propertyPanelFlatEffectSpecs.ts +305 -0
  31. package/src/components/editor/propertyPanelFlatEffectsSection.test.tsx +331 -0
  32. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +503 -0
  33. package/src/components/editor/propertyPanelFlatOverlaysSection.test.tsx +113 -0
  34. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +108 -0
  35. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +5 -5
  36. package/src/components/editor/propertyPanelFlatPrimitives.tsx +2 -0
  37. package/src/components/editor/propertyPanelPresetPreview.ts +36 -0
  38. package/src/components/editor/propertyPanelTypes.ts +13 -0
  39. package/src/components/editor/useColorGradingController.test.ts +235 -30
  40. package/src/components/editor/useColorGradingController.ts +46 -88
  41. package/src/components/editor/useColorGradingPreviews.ts +307 -0
  42. package/src/components/nle/NLEContext.tsx +1 -1
  43. package/src/contexts/PanelLayoutContext.tsx +3 -6
  44. package/src/hooks/useBlockCatalog.ts +31 -13
  45. package/src/hooks/useBlockHandlers.ts +22 -0
  46. package/src/hooks/useGsapScriptCommits.test.tsx +15 -0
  47. package/src/hooks/useGsapScriptCommits.ts +14 -1
  48. package/src/hooks/usePanelLayout.test.ts +68 -1
  49. package/src/hooks/usePanelLayout.ts +72 -34
  50. package/src/player/components/PlayerControls.tsx +68 -229
  51. package/src/player/components/ShortcutsPanel.tsx +2 -4
  52. package/src/player/components/SpeedMenu.tsx +1 -2
  53. package/src/telemetry/events.test.ts +9 -0
  54. package/src/telemetry/events.ts +5 -0
  55. package/src/utils/blockInstaller.test.ts +67 -0
  56. package/src/utils/blockInstaller.ts +162 -109
  57. package/src/utils/studioUiPreferences.test.ts +5 -0
  58. package/src/utils/studioUiPreferences.ts +8 -0
  59. package/dist/assets/index-CAeU317U.js +0 -428
  60. package/dist/assets/index-Ceyz8Qt2.css +0 -1
  61. package/src/player/components/PlayerControls.test.ts +0 -20
  62. package/src/player/components/useSeekBarDrag.ts +0 -169
@@ -30,7 +30,7 @@ interface AddBlockOptions {
30
30
  projectId: string;
31
31
  blockName: string;
32
32
  activeCompPath: string | null;
33
- placement?: { start: number; track: number };
33
+ placement?: { start: number; duration?: number; track?: number };
34
34
  visualPosition?: { left: number; top: number };
35
35
  previewIframe?: HTMLIFrameElement | null;
36
36
  currentTime?: number;
@@ -56,6 +56,121 @@ function buildUniqueCompositionId(baseName: string, existingIds: Iterable<string
56
56
  return `${baseName}_${i}`;
57
57
  }
58
58
 
59
+ async function installRegistryItem({
60
+ projectId,
61
+ blockName,
62
+ showToast,
63
+ }: Pick<AddBlockOptions, "projectId" | "blockName" | "showToast">): Promise<{
64
+ block: RegistryItem;
65
+ compositionFile: string;
66
+ } | null> {
67
+ const response = await fetch(`/api/projects/${projectId}/registry/install`, {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify({ blockName }),
71
+ });
72
+ if (!response.ok) {
73
+ const error = await response.json().catch(() => ({ error: "Install failed" }));
74
+ showToast((error as { error?: string }).error || "Failed to install block");
75
+ return null;
76
+ }
77
+ const { written, block } = (await response.json()) as {
78
+ written: string[];
79
+ block: RegistryItem;
80
+ };
81
+ const compositionFile = written.find((file) => file.endsWith(".html")) ?? written[0];
82
+ if (!compositionFile) {
83
+ showToast("Installed but no composition file was written");
84
+ return null;
85
+ }
86
+ return { block, compositionFile };
87
+ }
88
+
89
+ async function makeComponentBackgroundTransparent(
90
+ block: RegistryItem,
91
+ compositionFile: string,
92
+ readProjectFile: AddBlockOptions["readProjectFile"],
93
+ writeProjectFile: AddBlockOptions["writeProjectFile"],
94
+ ): Promise<void> {
95
+ 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);
102
+ }
103
+
104
+ function resolveBlockPlacement({
105
+ block,
106
+ placement,
107
+ timelineElements,
108
+ currentTime,
109
+ }: {
110
+ block: RegistryItem;
111
+ placement: AddBlockOptions["placement"];
112
+ timelineElements: TimelineElement[];
113
+ currentTime: number;
114
+ }) {
115
+ const isBlock = block.type === "hyperframes:block";
116
+ const {
117
+ start: placementStart = currentTime,
118
+ duration: placementDuration,
119
+ track: placementTrack,
120
+ } = placement ?? {};
121
+ const start = Number(formatTimelineAttributeNumber(placementStart));
122
+ const blockDuration = "duration" in block ? (block as { duration: number }).duration : undefined;
123
+ const duration =
124
+ placementDuration ??
125
+ blockDuration ??
126
+ timelineElements.reduce(
127
+ (max, element) => Math.max(max, (element.start ?? 0) + (element.duration ?? 0)),
128
+ 10,
129
+ );
130
+ const nextTrack = Math.max(0, ...timelineElements.map((element) => element.track)) + 1;
131
+ const track = placementTrack ?? (isBlock ? 0 : nextTrack);
132
+ return { duration, isBlock, start, track };
133
+ }
134
+
135
+ function buildSubCompositionHtml({
136
+ id,
137
+ compositionFile,
138
+ start,
139
+ duration,
140
+ track,
141
+ width,
142
+ height,
143
+ left,
144
+ top,
145
+ zIndex,
146
+ }: {
147
+ id: string;
148
+ compositionFile: string;
149
+ start: number;
150
+ duration: number;
151
+ track: number;
152
+ width: number;
153
+ height: number;
154
+ left: number;
155
+ top: number;
156
+ zIndex: number;
157
+ }): string {
158
+ return [
159
+ `<div`,
160
+ ` id="${id}"`,
161
+ ` data-hf-id="hf-${generateId()}"`,
162
+ ` data-composition-id="${id}"`,
163
+ ` data-composition-src="${compositionFile}"`,
164
+ ` data-start="${formatTimelineAttributeNumber(start)}"`,
165
+ ` data-duration="${formatTimelineAttributeNumber(duration)}"`,
166
+ ` data-track-index="${track}"`,
167
+ ` data-width="${width}"`,
168
+ ` data-height="${height}"`,
169
+ ` style="position: absolute; left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px; z-index: ${zIndex}"`,
170
+ `></div>`,
171
+ ].join("\n");
172
+ }
173
+
59
174
  export async function addBlockToProject(
60
175
  opts: AddBlockOptions,
61
176
  ): Promise<{ block: RegistryItem; compositionPath: string } | null> {
@@ -75,115 +190,53 @@ export async function addBlockToProject(
75
190
  } = opts;
76
191
 
77
192
  try {
78
- const res = await fetch(`/api/projects/${projectId}/registry/install`, {
79
- method: "POST",
80
- headers: { "Content-Type": "application/json" },
81
- body: JSON.stringify({ blockName }),
193
+ const installed = await installRegistryItem({ projectId, blockName, showToast });
194
+ if (!installed) return null;
195
+ const { block, compositionFile } = installed;
196
+ await makeComponentBackgroundTransparent(
197
+ block,
198
+ compositionFile,
199
+ readProjectFile,
200
+ writeProjectFile,
201
+ );
202
+
203
+ const targetPath = activeCompPath || "index.html";
204
+ const originalContent = await readProjectFile(targetPath);
205
+ const relevantElements = timelineElements.filter(
206
+ (element) => (element.sourceFile || targetPath) === targetPath,
207
+ );
208
+ const { duration, isBlock, start, track } = resolveBlockPlacement({
209
+ block,
210
+ placement,
211
+ timelineElements: relevantElements,
212
+ currentTime: opts.currentTime ?? 0,
213
+ });
214
+ const { width, height } = resolveTimelineAssetCompositionSize(originalContent);
215
+ const subComposition = buildSubCompositionHtml({
216
+ id: buildUniqueCompositionId(block.name, collectHtmlIds(originalContent)),
217
+ compositionFile,
218
+ start,
219
+ duration,
220
+ track,
221
+ width,
222
+ height,
223
+ left: visualPosition ? Math.round(visualPosition.left) : 0,
224
+ top: visualPosition ? Math.round(visualPosition.top) : 0,
225
+ zIndex: getMaxZIndexFromIframe(opts.previewIframe ?? null) + 1,
226
+ });
227
+ const patchedContent = extendRootDurationInSource(
228
+ insertTimelineAssetIntoSource(originalContent, subComposition),
229
+ start + duration,
230
+ );
231
+ await saveProjectFilesWithHistory({
232
+ projectId,
233
+ label: `Add ${isBlock ? "block" : "component"}: ${block.title}`,
234
+ kind: "timeline",
235
+ files: { [targetPath]: patchedContent },
236
+ readFile: async () => originalContent,
237
+ writeFile: writeProjectFile,
238
+ recordEdit,
82
239
  });
83
-
84
- if (!res.ok) {
85
- const err = await res.json().catch(() => ({ error: "Install failed" }));
86
- showToast((err as { error?: string }).error || "Failed to install block");
87
- return null;
88
- }
89
-
90
- const { written, block } = (await res.json()) as {
91
- written: string[];
92
- block: RegistryItem;
93
- };
94
-
95
- const compositionFile = written.find((f) => f.endsWith(".html")) ?? written[0];
96
- if (!compositionFile) {
97
- showToast("Installed but no composition file was written");
98
- return null;
99
- }
100
-
101
- if (block.type === "hyperframes:component") {
102
- const compContent = await readProjectFile(compositionFile);
103
- const transparentContent = compContent.replace(
104
- /background:\s*(?:#(?:0a0a0a|000000|000|0a0805)|rgba?\([^)]*\))\s*;/g,
105
- "background: transparent;",
106
- );
107
- if (transparentContent !== compContent) {
108
- await writeProjectFile(compositionFile, transparentContent);
109
- }
110
- }
111
-
112
- {
113
- const targetPath = activeCompPath || "index.html";
114
- const originalContent = await readProjectFile(targetPath);
115
- const existingIds = collectHtmlIds(originalContent);
116
- const compId = buildUniqueCompositionId(block.name, existingIds);
117
-
118
- const resolvedTargetPath = targetPath || "index.html";
119
- const relevantElements = timelineElements.filter(
120
- (te) => (te.sourceFile || activeCompPath || "index.html") === resolvedTargetPath,
121
- );
122
-
123
- const isBlock = block.type === "hyperframes:block";
124
- const { width: hostWidth, height: hostHeight } =
125
- resolveTimelineAssetCompositionSize(originalContent);
126
- const hostDims = { left: 0, top: 0, width: hostWidth, height: hostHeight };
127
-
128
- const currentTime = opts.currentTime ?? 0;
129
- const start = placement
130
- ? Number(formatTimelineAttributeNumber(placement.start))
131
- : Number(formatTimelineAttributeNumber(currentTime));
132
- const blockDuration =
133
- "duration" in block ? (block as { duration: number }).duration : undefined;
134
- const duration =
135
- blockDuration ??
136
- relevantElements.reduce(
137
- (max, te) => Math.max(max, (te.start ?? 0) + (te.duration ?? 0)),
138
- 10,
139
- );
140
- const track =
141
- placement?.track ??
142
- (isBlock
143
- ? 0
144
- : relevantElements.length > 0
145
- ? Math.max(...relevantElements.map((te) => te.track)) + 1
146
- : 1);
147
-
148
- const zIndex = getMaxZIndexFromIframe(opts.previewIframe ?? null) + 1;
149
-
150
- const width = hostDims.width;
151
- const height = hostDims.height;
152
-
153
- const left = visualPosition ? Math.round(visualPosition.left) : 0;
154
- const top = visualPosition ? Math.round(visualPosition.top) : 0;
155
-
156
- const subCompHtml = [
157
- `<div`,
158
- // A stable id (+ hf-id) is what authored sub-comps carry; without it the
159
- // timeline can't dedup the host and renders duplicate clips that multiply
160
- // on every interaction. Matches the authored-comp shape.
161
- ` id="${compId}"`,
162
- ` data-hf-id="hf-${generateId()}"`,
163
- ` data-composition-id="${compId}"`,
164
- ` data-composition-src="${compositionFile}"`,
165
- ` data-start="${formatTimelineAttributeNumber(start)}"`,
166
- ` data-duration="${formatTimelineAttributeNumber(duration)}"`,
167
- ` data-track-index="${track}"`,
168
- ` data-width="${width}"`,
169
- ` data-height="${height}"`,
170
- ` style="position: absolute; left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px; z-index: ${zIndex}"`,
171
- `></div>`,
172
- ].join("\n");
173
-
174
- let patchedContent = insertTimelineAssetIntoSource(originalContent, subCompHtml);
175
- patchedContent = extendRootDurationInSource(patchedContent, start + duration);
176
-
177
- await saveProjectFilesWithHistory({
178
- projectId,
179
- label: `Add ${isBlock ? "block" : "component"}: ${block.title}`,
180
- kind: "timeline",
181
- files: { [targetPath]: patchedContent },
182
- readFile: async () => originalContent,
183
- writeFile: writeProjectFile,
184
- recordEdit,
185
- });
186
- }
187
240
 
188
241
  await refreshFileTree();
189
242
  reloadPreview();
@@ -20,12 +20,15 @@ describe("studio UI preferences", () => {
20
20
  const storage = createStorage();
21
21
 
22
22
  writeStudioUiPreferences({ timelineVisible: false }, storage);
23
+ writeStudioUiPreferences({ leftWidth: 384, rightWidth: 424 }, storage);
23
24
  writeStudioUiPreferences({ playbackRate: 1.5 }, storage);
24
25
  writeStudioUiPreferences({ audioMuted: true }, storage);
25
26
  writeStudioUiPreferences({ previewZoom: { zoomPercent: 160, panX: -20, panY: 12 } }, storage);
26
27
 
27
28
  expect(readStudioUiPreferences(storage)).toEqual({
28
29
  timelineVisible: false,
30
+ leftWidth: 384,
31
+ rightWidth: 424,
29
32
  playbackRate: 1.5,
30
33
  audioMuted: true,
31
34
  previewZoom: { zoomPercent: 160, panX: -20, panY: 12 },
@@ -38,6 +41,8 @@ describe("studio UI preferences", () => {
38
41
  "hf-studio-ui-preferences",
39
42
  JSON.stringify({
40
43
  leftCollapsed: "yes",
44
+ leftWidth: "wide",
45
+ rightWidth: Number.NaN,
41
46
  timelineVisible: true,
42
47
  playbackRate: Number.NaN,
43
48
  audioMuted: "false",
@@ -6,6 +6,8 @@ export interface StoredPreviewZoomState {
6
6
 
7
7
  export interface StudioUiPreferences {
8
8
  leftCollapsed?: boolean;
9
+ leftWidth?: number;
10
+ rightWidth?: number;
9
11
  timelineVisible?: boolean;
10
12
  timelineHeight?: number;
11
13
  playbackRate?: number;
@@ -58,6 +60,12 @@ function readStorage(storage: Storage | null): StudioUiPreferences {
58
60
  if (typeof parsed.leftCollapsed === "boolean") {
59
61
  preferences.leftCollapsed = parsed.leftCollapsed;
60
62
  }
63
+ if (typeof parsed.leftWidth === "number" && Number.isFinite(parsed.leftWidth)) {
64
+ preferences.leftWidth = parsed.leftWidth;
65
+ }
66
+ if (typeof parsed.rightWidth === "number" && Number.isFinite(parsed.rightWidth)) {
67
+ preferences.rightWidth = parsed.rightWidth;
68
+ }
61
69
  if (typeof parsed.timelineVisible === "boolean") {
62
70
  preferences.timelineVisible = parsed.timelineVisible;
63
71
  }