@hyperframes/studio 0.5.0-alpha.9 → 0.5.0

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 (65) hide show
  1. package/dist/assets/hyperframes-player-CoI5h1xv.js +353 -0
  2. package/dist/assets/index-BKjcNNNd.css +1 -0
  3. package/dist/assets/index-CqiisJmo.js +93 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +208 -1438
  7. package/src/captions/generator.test.ts +19 -0
  8. package/src/captions/generator.ts +9 -2
  9. package/src/captions/hooks/useCaptionSync.ts +6 -1
  10. package/src/captions/parser.test.ts +14 -0
  11. package/src/captions/parser.ts +1 -0
  12. package/src/components/LintModal.tsx +4 -3
  13. package/src/components/editor/PropertyPanel.tsx +206 -2466
  14. package/src/components/nle/NLELayout.tsx +47 -17
  15. package/src/components/nle/NLEPreview.tsx +5 -50
  16. package/src/components/sidebar/AssetsTab.tsx +4 -3
  17. package/src/components/sidebar/CompositionsTab.test.ts +1 -16
  18. package/src/components/sidebar/CompositionsTab.tsx +45 -117
  19. package/src/components/sidebar/LeftSidebar.tsx +55 -34
  20. package/src/components/ui/HyperframesLoader.tsx +104 -0
  21. package/src/components/ui/index.ts +2 -0
  22. package/src/icons/SystemIcons.tsx +2 -0
  23. package/src/player/components/CompositionThumbnail.tsx +10 -42
  24. package/src/player/components/EditModal.tsx +20 -5
  25. package/src/player/components/Player.tsx +129 -28
  26. package/src/player/components/PlayerControls.tsx +3 -44
  27. package/src/player/components/Timeline.test.ts +0 -12
  28. package/src/player/components/Timeline.tsx +25 -52
  29. package/src/player/components/TimelineClip.tsx +9 -21
  30. package/src/player/components/timelineEditing.test.ts +4 -2
  31. package/src/player/components/timelineEditing.ts +3 -1
  32. package/src/player/components/timelineTheme.test.ts +19 -0
  33. package/src/player/components/timelineTheme.ts +8 -4
  34. package/src/player/hooks/useTimelinePlayer.test.ts +160 -21
  35. package/src/player/hooks/useTimelinePlayer.ts +206 -93
  36. package/src/player/lib/time.test.ts +11 -1
  37. package/src/player/lib/time.ts +6 -0
  38. package/src/player/store/playerStore.ts +1 -0
  39. package/src/styles/studio.css +112 -0
  40. package/src/utils/frameCapture.test.ts +26 -0
  41. package/src/utils/frameCapture.ts +40 -0
  42. package/src/utils/mediaTypes.ts +1 -1
  43. package/src/utils/projectRouting.test.ts +87 -0
  44. package/src/utils/projectRouting.ts +27 -0
  45. package/src/utils/sourcePatcher.test.ts +1 -128
  46. package/src/utils/sourcePatcher.ts +18 -130
  47. package/src/utils/timelineAssetDrop.test.ts +11 -31
  48. package/src/utils/timelineAssetDrop.ts +2 -22
  49. package/dist/assets/hyperframes-player-vibA20NC.js +0 -198
  50. package/dist/assets/index-DKaNgV2Z.css +0 -1
  51. package/dist/assets/index-peNJzL-4.js +0 -105
  52. package/src/components/editor/DomEditOverlay.tsx +0 -445
  53. package/src/components/editor/colorValue.test.ts +0 -82
  54. package/src/components/editor/colorValue.ts +0 -175
  55. package/src/components/editor/domEditing.test.ts +0 -537
  56. package/src/components/editor/domEditing.ts +0 -762
  57. package/src/components/editor/floatingPanel.test.ts +0 -34
  58. package/src/components/editor/floatingPanel.ts +0 -54
  59. package/src/components/editor/fontAssets.ts +0 -32
  60. package/src/components/editor/fontCatalog.ts +0 -126
  61. package/src/components/editor/gradientValue.test.ts +0 -89
  62. package/src/components/editor/gradientValue.ts +0 -445
  63. package/src/player/components/CompositionThumbnail.test.ts +0 -19
  64. package/src/utils/clipboard.test.ts +0 -88
  65. package/src/utils/clipboard.ts +0 -57
@@ -61,26 +61,8 @@ export const TimelineClip = memo(function TimelineClip({
61
61
  ? theme.clipShadowHover
62
62
  : theme.clipShadow;
63
63
  const capabilities = getTimelineEditCapabilities(el);
64
+ const displayLabel = el.label || el.id || el.tag;
64
65
  const showHandles = handleOpacity > 0.01;
65
- const baseBackgroundImage = isSelected ? theme.clipBackgroundActive : theme.clipBackground;
66
- const glossBackgroundImage = isSelected
67
- ? "linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0))"
68
- : "linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0))";
69
- const accentBackgroundImage = `linear-gradient(120deg, ${trackStyle.accent}${
70
- isSelected ? "22" : "1e"
71
- }, transparent 28%)`;
72
- const compositionStripeBackgroundImage =
73
- isComposition && !hasCustomContent
74
- ? "repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.05) 3px, rgba(255,255,255,0.05) 6px)"
75
- : undefined;
76
- const clipBackgroundImage = [
77
- compositionStripeBackgroundImage,
78
- glossBackgroundImage,
79
- accentBackgroundImage,
80
- baseBackgroundImage,
81
- ]
82
- .filter(Boolean)
83
- .join(", ");
84
66
 
85
67
  return (
86
68
  <div
@@ -94,7 +76,13 @@ export const TimelineClip = memo(function TimelineClip({
94
76
  top: clipY,
95
77
  bottom: clipY,
96
78
  borderRadius: theme.clipRadius,
97
- backgroundImage: clipBackgroundImage,
79
+ background: isSelected
80
+ ? `linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0)), linear-gradient(120deg, ${trackStyle.accent}22, transparent 28%), ${theme.clipBackgroundActive}`
81
+ : `linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)), linear-gradient(120deg, ${trackStyle.accent}1e, transparent 28%), ${theme.clipBackground}`,
82
+ backgroundImage:
83
+ isComposition && !hasCustomContent
84
+ ? `repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.05) 3px, rgba(255,255,255,0.05) 6px)`
85
+ : undefined,
98
86
  border: `1px solid ${borderColor}`,
99
87
  boxShadow,
100
88
  transition:
@@ -106,7 +94,7 @@ export const TimelineClip = memo(function TimelineClip({
106
94
  title={
107
95
  isComposition
108
96
  ? `${el.compositionSrc} \u2022 Double-click to open`
109
- : `${el.id || el.tag} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`
97
+ : `${displayLabel} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`
110
98
  }
111
99
  onPointerEnter={onHoverStart}
112
100
  onPointerLeave={onHoverEnd}
@@ -248,7 +248,7 @@ describe("getTimelineEditCapabilities", () => {
248
248
  });
249
249
  });
250
250
 
251
- it("allows moving generic motion clips while keeping trims blocked", () => {
251
+ it("disables move and trims for generic motion clips even when patchable", () => {
252
252
  expect(
253
253
  getTimelineEditCapabilities({
254
254
  tag: "section",
@@ -256,7 +256,7 @@ describe("getTimelineEditCapabilities", () => {
256
256
  selector: ".feature-card",
257
257
  }),
258
258
  ).toEqual({
259
- canMove: true,
259
+ canMove: false,
260
260
  canTrimStart: false,
261
261
  canTrimEnd: false,
262
262
  });
@@ -428,6 +428,7 @@ describe("buildClipRangeSelection", () => {
428
428
  });
429
429
  });
430
430
  });
431
+
431
432
  describe("resolveTimelineAutoScroll", () => {
432
433
  it("does not scroll when the pointer stays away from the edges", () => {
433
434
  expect(
@@ -511,6 +512,7 @@ describe("buildTimelineElementAgentPrompt", () => {
511
512
  ).toContain("If this clip is animated with GSAP");
512
513
  });
513
514
  });
515
+
514
516
  describe("resolveTimelineResize", () => {
515
517
  it("shrinks clip duration from the right edge", () => {
516
518
  expect(
@@ -233,7 +233,7 @@ export function getTimelineEditCapabilities(input: {
233
233
  const hasFiniteDuration = Number.isFinite(input.duration) && input.duration > 0;
234
234
  const hasDeterministicWindow = isDeterministicTimelineWindow(input);
235
235
  return {
236
- canMove: canPatch && (hasDeterministicWindow || hasFiniteDuration),
236
+ canMove: canPatch && hasDeterministicWindow,
237
237
  canTrimEnd: canPatch && hasFiniteDuration && hasDeterministicWindow,
238
238
  canTrimStart: canPatch && hasFiniteDuration && canOffsetTrimClipStart(input),
239
239
  };
@@ -273,6 +273,7 @@ export function buildClipRangeSelection(
273
273
  anchorY: anchor.anchorY,
274
274
  };
275
275
  }
276
+
276
277
  export function buildTimelineAgentPrompt({
277
278
  rangeStart,
278
279
  rangeEnd,
@@ -346,6 +347,7 @@ export function buildTimelineElementAgentPrompt(element: {
346
347
 
347
348
  return lines.join("\n");
348
349
  }
350
+
349
351
  export function formatTimelineAttributeNumber(value: number): string {
350
352
  return Number(roundToCentiseconds(value).toFixed(2)).toString();
351
353
  }
@@ -53,4 +53,23 @@ describe("getRenderedTimelineElement", () => {
53
53
  }),
54
54
  ).toEqual({ ...element, start: 2.4, track: 3 });
55
55
  });
56
+
57
+ it("uses key before id when matching the dragged clip", () => {
58
+ const element = {
59
+ id: "Card",
60
+ key: "index.html:.card:1",
61
+ tag: "div",
62
+ start: 1,
63
+ duration: 2,
64
+ track: 0,
65
+ };
66
+ expect(
67
+ getRenderedTimelineElement({
68
+ element,
69
+ draggedElementId: "index.html:.card:1",
70
+ previewStart: 2.4,
71
+ previewTrack: 3,
72
+ }),
73
+ ).toEqual({ ...element, start: 2.4, track: 3 });
74
+ });
56
75
  });
@@ -63,12 +63,12 @@ const TRACK_STYLES: Record<string, TimelineTrackStyle> = {
63
63
  const DEFAULT_TRACK_STYLE: TimelineTrackStyle = createTrackStyle();
64
64
 
65
65
  export const defaultTimelineTheme: TimelineTheme = {
66
- shellBackground: "#0A0E15",
66
+ shellBackground: "#0A0A0B",
67
67
  shellBorder: "rgba(255,255,255,0.05)",
68
68
  rulerBorder: "rgba(255,255,255,0.045)",
69
- rowBackground: "#0A0E15",
69
+ rowBackground: "#0A0A0B",
70
70
  rowBorder: "rgba(255,255,255,0.05)",
71
- gutterBackground: "#0D121B",
71
+ gutterBackground: "#0A0A0B",
72
72
  gutterBorder: "rgba(255,255,255,0.05)",
73
73
  textPrimary: "#E8EDF5",
74
74
  textSecondary: "#8391A8",
@@ -130,7 +130,11 @@ export function getRenderedTimelineElement({
130
130
  previewStart: number | null;
131
131
  previewTrack: number | null;
132
132
  }): TimelineElement {
133
- if (element.id !== draggedElementId || previewStart === null || previewTrack === null) {
133
+ if (
134
+ (element.key ?? element.id) !== draggedElementId ||
135
+ previewStart === null ||
136
+ previewTrack === null
137
+ ) {
134
138
  return element;
135
139
  }
136
140
  return {
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
2
2
  import { Window } from "happy-dom";
3
3
  import {
4
4
  buildStandaloneRootTimelineElement,
5
+ createTimelineElementFromManifestClip,
5
6
  findTimelineDomNodeForClip,
6
7
  getTimelineElementSelector,
8
+ parseTimelineFromDOM,
7
9
  type ClipManifestClip,
8
10
  mergeTimelineElementsPreservingDowngrades,
9
11
  resolveStandaloneRootCompositionSrc,
@@ -11,6 +13,26 @@ import {
11
13
  shouldIgnorePlaybackShortcutTarget,
12
14
  } from "./useTimelinePlayer";
13
15
 
16
+ function mockTargetMatching(selectorNeedle: string): EventTarget {
17
+ return {
18
+ closest: (selector: string) => (selector.includes(selectorNeedle) ? ({} as Element) : null),
19
+ } as unknown as EventTarget;
20
+ }
21
+
22
+ function mockKeyboardEvent(
23
+ code: string,
24
+ overrides: Partial<Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "target">> = {},
25
+ ): Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "code" | "target"> {
26
+ return {
27
+ altKey: false,
28
+ ctrlKey: false,
29
+ metaKey: false,
30
+ code,
31
+ target: mockTargetMatching("[data-missing]"),
32
+ ...overrides,
33
+ };
34
+ }
35
+
14
36
  function createDocument(markup: string): Document {
15
37
  const window = new Window();
16
38
  window.document.body.innerHTML = markup;
@@ -20,7 +42,7 @@ function createDocument(markup: string): Document {
20
42
  function createClip(overrides: Partial<ClipManifestClip>): ClipManifestClip {
21
43
  return {
22
44
  id: null,
23
- label: "",
45
+ label: "Element",
24
46
  start: 0,
25
47
  duration: 4,
26
48
  track: 0,
@@ -34,26 +56,6 @@ function createClip(overrides: Partial<ClipManifestClip>): ClipManifestClip {
34
56
  };
35
57
  }
36
58
 
37
- function mockTargetMatching(selectorNeedle: string): EventTarget {
38
- return {
39
- closest: (selector: string) => (selector.includes(selectorNeedle) ? ({} as Element) : null),
40
- } as unknown as EventTarget;
41
- }
42
-
43
- function mockKeyboardEvent(
44
- code: string,
45
- overrides: Partial<Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "target">> = {},
46
- ): Pick<KeyboardEvent, "altKey" | "ctrlKey" | "metaKey" | "code" | "target"> {
47
- return {
48
- altKey: false,
49
- ctrlKey: false,
50
- metaKey: false,
51
- code,
52
- target: mockTargetMatching("[data-missing]"),
53
- ...overrides,
54
- };
55
- }
56
-
57
59
  describe("buildStandaloneRootTimelineElement", () => {
58
60
  it("includes selector and source metadata for standalone composition fallback clips", () => {
59
61
  expect(
@@ -66,6 +68,7 @@ describe("buildStandaloneRootTimelineElement", () => {
66
68
  }),
67
69
  ).toEqual({
68
70
  id: "hero",
71
+ label: "hero",
69
72
  key: 'scenes/hero.html:[data-composition-id="hero"]:0',
70
73
  tag: "div",
71
74
  start: 0,
@@ -146,6 +149,83 @@ describe("findTimelineDomNodeForClip", () => {
146
149
  });
147
150
  });
148
151
 
152
+ describe("anonymous timeline identity", () => {
153
+ it("keeps fallback-parsed anonymous clips distinct when labels match", () => {
154
+ const doc = createDocument(`
155
+ <div data-composition-id="main" data-start="0" data-duration="8">
156
+ <div class="clip card" data-label="Card" data-start="0" data-duration="3" data-track-index="0"></div>
157
+ <div class="clip card" data-label="Card" data-start="3" data-duration="3" data-track-index="1"></div>
158
+ </div>
159
+ `);
160
+
161
+ const elements = parseTimelineFromDOM(doc, 8);
162
+
163
+ expect(elements).toHaveLength(2);
164
+ expect(elements.map((element) => element.label)).toEqual(["Card", "Card"]);
165
+ expect(new Set(elements.map((element) => element.id)).size).toBe(2);
166
+ expect(new Set(elements.map((element) => element.key)).size).toBe(2);
167
+ expect(elements.map((element) => element.selectorIndex)).toEqual([0, 1]);
168
+ });
169
+
170
+ it("keeps runtime-manifest anonymous clips distinct when labels match", () => {
171
+ const doc = createDocument(`
172
+ <div data-composition-id="main" data-start="0" data-duration="8">
173
+ <div class="clip card" data-start="0" data-duration="3" data-track-index="0"></div>
174
+ <div class="clip card" data-start="3" data-duration="3" data-track-index="1"></div>
175
+ </div>
176
+ `);
177
+ const clips = [
178
+ createClip({ id: null, label: "Card", start: 0, duration: 3, track: 0 }),
179
+ createClip({ id: null, label: "Card", start: 3, duration: 3, track: 1 }),
180
+ ];
181
+ const used = new Set<Element>();
182
+ const elements = clips.map((clip, index) => {
183
+ const hostEl = findTimelineDomNodeForClip(doc, clip, index, used);
184
+ if (hostEl) used.add(hostEl);
185
+ return createTimelineElementFromManifestClip({
186
+ clip,
187
+ fallbackIndex: index,
188
+ doc,
189
+ hostEl,
190
+ });
191
+ });
192
+
193
+ expect(elements.map((element) => element.label)).toEqual(["Card", "Card"]);
194
+ expect(new Set(elements.map((element) => element.id)).size).toBe(2);
195
+ expect(new Set(elements.map((element) => element.key)).size).toBe(2);
196
+ expect(elements.map((element) => element.selectorIndex)).toEqual([0, 1]);
197
+ });
198
+
199
+ it("reads media metadata from owner-window media elements", () => {
200
+ const doc = createDocument(`
201
+ <div data-composition-id="main" data-start="0" data-duration="8">
202
+ <div class="clip video-card" data-start="0" data-duration="3" data-track-index="0">
203
+ <video src="/clip.mp4" data-source-duration="12"></video>
204
+ </div>
205
+ </div>
206
+ `);
207
+ const hostEl = doc.querySelector(".video-card");
208
+ const video = hostEl?.querySelector("video");
209
+ if (!hostEl || !video) throw new Error("missing video test fixture");
210
+ Object.defineProperty(video, "defaultPlaybackRate", {
211
+ value: 1.5,
212
+ configurable: true,
213
+ });
214
+
215
+ const element = createTimelineElementFromManifestClip({
216
+ clip: createClip({ kind: "video", tagName: "div" }),
217
+ fallbackIndex: 0,
218
+ doc,
219
+ hostEl,
220
+ });
221
+
222
+ expect(element.tag).toBe("video");
223
+ expect(element.src).toBe("/clip.mp4");
224
+ expect(element.sourceDuration).toBe(12);
225
+ expect(element.playbackRate).toBe(1.5);
226
+ });
227
+ });
228
+
149
229
  describe("mergeTimelineElementsPreservingDowngrades", () => {
150
230
  it("preserves missing current elements when a shorter manifest arrives", () => {
151
231
  expect(
@@ -174,6 +254,65 @@ describe("mergeTimelineElementsPreservingDowngrades", () => {
174
254
  ),
175
255
  ).toEqual([{ id: "hero", tag: "div", start: 0, duration: 4, track: 0 }]);
176
256
  });
257
+
258
+ it("preserves distinct anonymous clips that share the same friendly id label", () => {
259
+ expect(
260
+ mergeTimelineElementsPreservingDowngrades(
261
+ [
262
+ {
263
+ id: "Card",
264
+ key: "index.html:.card:0",
265
+ label: "Card",
266
+ tag: "div",
267
+ start: 0,
268
+ duration: 3,
269
+ track: 0,
270
+ },
271
+ {
272
+ id: "Card",
273
+ key: "index.html:.card:1",
274
+ label: "Card",
275
+ tag: "div",
276
+ start: 3,
277
+ duration: 3,
278
+ track: 1,
279
+ },
280
+ ],
281
+ [
282
+ {
283
+ id: "Card",
284
+ key: "index.html:.card:0",
285
+ label: "Card",
286
+ tag: "div",
287
+ start: 0,
288
+ duration: 3,
289
+ track: 0,
290
+ },
291
+ ],
292
+ 8,
293
+ 8,
294
+ ),
295
+ ).toEqual([
296
+ {
297
+ id: "Card",
298
+ key: "index.html:.card:0",
299
+ label: "Card",
300
+ tag: "div",
301
+ start: 0,
302
+ duration: 3,
303
+ track: 0,
304
+ },
305
+ {
306
+ id: "Card",
307
+ key: "index.html:.card:1",
308
+ label: "Card",
309
+ tag: "div",
310
+ start: 3,
311
+ duration: 3,
312
+ track: 1,
313
+ },
314
+ ]);
315
+ });
177
316
  });
178
317
 
179
318
  describe("shouldIgnorePlaybackShortcutTarget", () => {