@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
@@ -137,6 +137,25 @@ describe("generateCaptionHtml", () => {
137
137
  expect(html).toContain('"end": 2.7');
138
138
  });
139
139
 
140
+ it("includes stable word ids in the transcript and generated word spans", () => {
141
+ const transcript: TranscriptWord[] = [
142
+ { id: "word-a", text: "Hello", start: 0, end: 0.4 },
143
+ { id: "word-b", text: "world", start: 0.5, end: 1 },
144
+ ];
145
+ const model = buildCaptionModel(transcript, {
146
+ width: 1920,
147
+ height: 1080,
148
+ duration: 2,
149
+ });
150
+
151
+ const html = generateCaptionHtml(model);
152
+
153
+ expect(html).toContain('"id": "word-a"');
154
+ expect(html).toContain('"id": "word-b"');
155
+ expect(html).toContain('w_segment_0.id = "word-a";');
156
+ expect(html).toContain('w_segment_1.id = "word-b";');
157
+ });
158
+
140
159
  it("TRANSCRIPT contains all 7 words from the sample", () => {
141
160
  const model = buildTestModel();
142
161
  const html = generateCaptionHtml(model);
@@ -261,14 +261,19 @@ function hexToRgba(color: string, opacity: number): string {
261
261
 
262
262
  function generateJs(model: CaptionModel): string {
263
263
  // Collect all segments across all groups in order
264
- const allSegments: Array<{ text: string; start: number; end: number }> = [];
264
+ const allSegments: Array<{ id?: string; text: string; start: number; end: number }> = [];
265
265
  for (const groupId of model.groupOrder) {
266
266
  const group = model.groups.get(groupId);
267
267
  if (!group) continue;
268
268
  for (const segId of group.segmentIds) {
269
269
  const seg = model.segments.get(segId);
270
270
  if (!seg) continue;
271
- allSegments.push({ text: seg.text, start: seg.start, end: seg.end });
271
+ allSegments.push({
272
+ ...(seg.wordId ? { id: seg.wordId } : {}),
273
+ text: seg.text,
274
+ start: seg.start,
275
+ end: seg.end,
276
+ });
272
277
  }
273
278
  }
274
279
 
@@ -300,9 +305,11 @@ function generateJs(model: CaptionModel): string {
300
305
  const wordLines: string[] = groupSegments.map((seg) => {
301
306
  const escaped = seg.text.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
302
307
  const segVar = `w_${seg.id.replace(/[^a-zA-Z0-9_]/g, "_")}`;
308
+ const idLine = seg.wordId ? `\n ${segVar}.id = ${JSON.stringify(seg.wordId)};` : "";
303
309
  return (
304
310
  ` const ${segVar} = document.createElement('span');` +
305
311
  `\n ${segVar}.className = 'word clip';` +
312
+ idLine +
306
313
  `\n ${segVar}.textContent = '${escaped}';` +
307
314
  `\n ${segVar}.dataset.start = '${seg.start}';` +
308
315
  `\n ${segVar}.dataset.end = '${seg.end}';` +
@@ -124,17 +124,22 @@ export function useCaptionSync(projectId: string | null) {
124
124
 
125
125
  const model = state.model;
126
126
  const allSegIds: string[] = [];
127
+ const segIdByWordId = new Map<string, string>();
127
128
  for (const groupId of model.groupOrder) {
128
129
  const group = model.groups.get(groupId);
129
130
  if (!group) continue;
130
131
  for (const segId of group.segmentIds) {
131
132
  allSegIds.push(segId);
133
+ const seg = model.segments.get(segId);
134
+ if (seg?.wordId) segIdByWordId.set(seg.wordId, segId);
132
135
  }
133
136
  }
134
137
 
135
138
  const newSegments = new Map(model.segments);
136
139
  for (const override of overrides) {
137
- const segId = allSegIds[override.wordIndex];
140
+ const segId =
141
+ (override.wordId ? segIdByWordId.get(override.wordId) : undefined) ??
142
+ allSegIds[override.wordIndex];
138
143
  if (!segId) continue;
139
144
  const seg = newSegments.get(segId);
140
145
  if (!seg) continue;
@@ -102,6 +102,20 @@ describe("extractTranscript", () => {
102
102
  expect(words).toHaveLength(1);
103
103
  expect(words[0]).toEqual({ text: "Hello", start: 0.0, end: 0.5 });
104
104
  });
105
+
106
+ it("preserves stable word ids when present", () => {
107
+ const words = extractTranscript(`
108
+ const TRANSCRIPT = [
109
+ { id: "word-a", text: "Hello", start: 0, end: 0.4 },
110
+ { id: "word-b", text: "world", start: 0.5, end: 1 },
111
+ ];
112
+ `);
113
+
114
+ expect(words).toEqual([
115
+ { id: "word-a", text: "Hello", start: 0, end: 0.4 },
116
+ { id: "word-b", text: "world", start: 0.5, end: 1 },
117
+ ]);
118
+ });
105
119
  });
106
120
 
107
121
  describe("script variable name", () => {
@@ -303,6 +303,7 @@ function parseTranscriptArray(arrayLiteral: string): TranscriptWord[] {
303
303
  ) {
304
304
  const entry = item as Record<string, unknown>;
305
305
  words.push({
306
+ ...(typeof entry.id === "string" ? { id: entry.id } : {}),
306
307
  text: entry.text as string,
307
308
  start: entry.start as number,
308
309
  end: entry.end as number,
@@ -1,6 +1,5 @@
1
1
  import { useState } from "react";
2
2
  import { XIcon, WarningIcon, CheckCircleIcon, CaretRightIcon } from "@phosphor-icons/react";
3
- import { copyTextToClipboard } from "../utils/clipboard";
4
3
 
5
4
  export interface LintFinding {
6
5
  severity: "error" | "warning";
@@ -31,10 +30,12 @@ export function LintModal({
31
30
  return line;
32
31
  });
33
32
  const text = `Fix these HyperFrames lint issues for project "${projectId}":\n\nProject path: ${window.location.href}\n\n${lines.join("\n\n")}`;
34
- const copiedText = await copyTextToClipboard(text);
35
- if (copiedText) {
33
+ try {
34
+ await navigator.clipboard.writeText(text);
36
35
  setCopied(true);
37
36
  setTimeout(() => setCopied(false), 2000);
37
+ } catch {
38
+ // ignore
38
39
  }
39
40
  };
40
41