@hyperframes/studio 0.5.0-alpha.8 → 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 (69) 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 -1436
  7. package/src/captions/components/CaptionOverlay.tsx +2 -1
  8. package/src/captions/generator.test.ts +19 -0
  9. package/src/captions/generator.ts +9 -2
  10. package/src/captions/hooks/useCaptionSync.ts +6 -1
  11. package/src/captions/keyboard.test.ts +38 -0
  12. package/src/captions/keyboard.ts +8 -0
  13. package/src/captions/parser.test.ts +14 -0
  14. package/src/captions/parser.ts +1 -0
  15. package/src/components/LintModal.tsx +4 -3
  16. package/src/components/editor/PropertyPanel.tsx +206 -2462
  17. package/src/components/nle/NLELayout.tsx +47 -17
  18. package/src/components/nle/NLEPreview.tsx +9 -50
  19. package/src/components/sidebar/AssetsTab.tsx +4 -3
  20. package/src/components/sidebar/CompositionsTab.test.ts +1 -16
  21. package/src/components/sidebar/CompositionsTab.tsx +45 -117
  22. package/src/components/sidebar/LeftSidebar.tsx +55 -34
  23. package/src/components/ui/HyperframesLoader.tsx +104 -0
  24. package/src/components/ui/index.ts +2 -0
  25. package/src/icons/SystemIcons.tsx +2 -0
  26. package/src/player/components/CompositionThumbnail.tsx +10 -42
  27. package/src/player/components/EditModal.tsx +20 -5
  28. package/src/player/components/Player.tsx +129 -28
  29. package/src/player/components/PlayerControls.tsx +117 -49
  30. package/src/player/components/Timeline.test.ts +0 -12
  31. package/src/player/components/Timeline.tsx +25 -52
  32. package/src/player/components/TimelineClip.tsx +9 -21
  33. package/src/player/components/timelineEditing.test.ts +4 -2
  34. package/src/player/components/timelineEditing.ts +3 -1
  35. package/src/player/components/timelineTheme.test.ts +19 -0
  36. package/src/player/components/timelineTheme.ts +8 -4
  37. package/src/player/hooks/useTimelinePlayer.test.ts +219 -1
  38. package/src/player/hooks/useTimelinePlayer.ts +487 -106
  39. package/src/player/lib/time.test.ts +29 -1
  40. package/src/player/lib/time.ts +26 -0
  41. package/src/player/store/playerStore.test.ts +11 -1
  42. package/src/player/store/playerStore.ts +6 -1
  43. package/src/styles/studio.css +112 -0
  44. package/src/utils/frameCapture.test.ts +26 -0
  45. package/src/utils/frameCapture.ts +40 -0
  46. package/src/utils/mediaTypes.ts +1 -1
  47. package/src/utils/projectRouting.test.ts +87 -0
  48. package/src/utils/projectRouting.ts +27 -0
  49. package/src/utils/sourcePatcher.test.ts +1 -128
  50. package/src/utils/sourcePatcher.ts +18 -130
  51. package/src/utils/timelineAssetDrop.test.ts +11 -31
  52. package/src/utils/timelineAssetDrop.ts +2 -22
  53. package/dist/assets/hyperframes-player-vibA20NC.js +0 -198
  54. package/dist/assets/index-0Zt0t13W.css +0 -1
  55. package/dist/assets/index-C9f5eif8.js +0 -105
  56. package/src/components/editor/DomEditOverlay.tsx +0 -442
  57. package/src/components/editor/colorValue.test.ts +0 -82
  58. package/src/components/editor/colorValue.ts +0 -175
  59. package/src/components/editor/domEditing.test.ts +0 -537
  60. package/src/components/editor/domEditing.ts +0 -762
  61. package/src/components/editor/floatingPanel.test.ts +0 -34
  62. package/src/components/editor/floatingPanel.ts +0 -54
  63. package/src/components/editor/fontAssets.ts +0 -32
  64. package/src/components/editor/fontCatalog.ts +0 -126
  65. package/src/components/editor/gradientValue.test.ts +0 -89
  66. package/src/components/editor/gradientValue.ts +0 -445
  67. package/src/player/components/CompositionThumbnail.test.ts +0 -19
  68. package/src/utils/clipboard.test.ts +0 -88
  69. package/src/utils/clipboard.ts +0 -57
@@ -7,67 +7,6 @@ function escapeRegex(s: string): string {
7
7
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8
8
  }
9
9
 
10
- function escapeStyleAttributeValue(value: string, quote: string): string {
11
- return quote === '"' ? value.replace(/"/g, """) : value.replace(/'/g, "'");
12
- }
13
-
14
- function splitInlineStyleDeclarations(style: string): string[] {
15
- const declarations: string[] = [];
16
- let current = "";
17
- let quote: string | null = null;
18
- let entity = false;
19
- let parenDepth = 0;
20
-
21
- for (const char of style) {
22
- if (entity) {
23
- current += char;
24
- if (char === ";") entity = false;
25
- continue;
26
- }
27
-
28
- if (char === "&") {
29
- entity = true;
30
- current += char;
31
- continue;
32
- }
33
-
34
- if (quote) {
35
- current += char;
36
- if (char === quote) quote = null;
37
- continue;
38
- }
39
-
40
- if (char === '"' || char === "'") {
41
- quote = char;
42
- current += char;
43
- continue;
44
- }
45
-
46
- if (char === "(") {
47
- parenDepth += 1;
48
- current += char;
49
- continue;
50
- }
51
-
52
- if (char === ")") {
53
- parenDepth = Math.max(0, parenDepth - 1);
54
- current += char;
55
- continue;
56
- }
57
-
58
- if (char === ";" && parenDepth === 0) {
59
- declarations.push(current);
60
- current = "";
61
- continue;
62
- }
63
-
64
- current += char;
65
- }
66
-
67
- if (current) declarations.push(current);
68
- return declarations;
69
- }
70
-
71
10
  export interface PatchOperation {
72
11
  type: "inline-style" | "attribute" | "text-content";
73
12
  property: string;
@@ -135,7 +74,7 @@ export function resolveSourceFile(
135
74
  */
136
75
  function patchInlineStyle(html: string, elementId: string, prop: string, value: string): string {
137
76
  // Find the element tag with this id
138
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(elementId)}\\2[^>]*)>`, "i");
77
+ const idPattern = new RegExp(`(<[^>]*\\bid="${escapeRegex(elementId)}"[^>]*)>`, "i");
139
78
  const match = idPattern.exec(html);
140
79
  if (!match) return html;
141
80
 
@@ -147,13 +86,12 @@ function patchInlineStyleInTag(html: string, tag: string, prop: string, value: s
147
86
  if (!tag) return html;
148
87
 
149
88
  // Check if there's an existing style attribute
150
- const styleMatch = /\bstyle=(["'])([\s\S]*?)\1/.exec(tag);
89
+ const styleMatch = /\bstyle="([^"]*)"/.exec(tag);
151
90
  if (styleMatch) {
152
- const existingStyle = styleMatch[2];
153
- const quote = styleMatch[1];
91
+ const existingStyle = styleMatch[1];
154
92
  // Parse existing properties
155
93
  const props = new Map<string, string>();
156
- for (const part of splitInlineStyleDeclarations(existingStyle)) {
94
+ for (const part of existingStyle.split(";")) {
157
95
  const colon = part.indexOf(":");
158
96
  if (colon < 0) continue;
159
97
  const key = part.slice(0, colon).trim();
@@ -164,14 +102,13 @@ function patchInlineStyleInTag(html: string, tag: string, prop: string, value: s
164
102
  props.set(prop, value);
165
103
  // Rebuild style string
166
104
  const newStyle = Array.from(props.entries())
167
- .map(([k, v]) => `${k}: ${escapeStyleAttributeValue(v, quote)}`)
105
+ .map(([k, v]) => `${k}: ${v}`)
168
106
  .join("; ");
169
- const newTag = tag.replace(styleMatch[0], `style=${quote}${newStyle}${quote}`);
107
+ const newTag = tag.replace(/\bstyle="[^"]*"/, `style="${newStyle}"`);
170
108
  return html.replace(tag, newTag);
171
109
  } else {
172
110
  // No existing style — add one
173
- const newTag =
174
- tag.replace(/>$/, "") + ` style="${prop}: ${escapeStyleAttributeValue(value, '"')}"`;
111
+ const newTag = tag.replace(/>$/, "") + ` style="${prop}: ${value}"`;
175
112
  return html.replace(tag, newTag);
176
113
  }
177
114
  }
@@ -200,7 +137,7 @@ function replaceTagAtMatch(html: string, match: TagMatch, newTag: string): strin
200
137
 
201
138
  function findTagByTarget(html: string, target: PatchTarget): TagMatch | null {
202
139
  if (target.id) {
203
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(target.id)}\\2[^>]*)>`, "i");
140
+ const idPattern = new RegExp(`(<[^>]*\\bid="${escapeRegex(target.id)}"[^>]*)>`, "i");
204
141
  const match = idPattern.exec(html);
205
142
  if (match?.index != null) {
206
143
  return {
@@ -217,7 +154,7 @@ function findTagByTarget(html: string, target: PatchTarget): TagMatch | null {
217
154
  if (compositionIdMatch) {
218
155
  const compId = compositionIdMatch[1];
219
156
  const pattern = new RegExp(
220
- `(<[^>]*\\bdata-composition-id=(["'])${escapeRegex(compId)}\\2[^>]*)>`,
157
+ `(<[^>]*\\bdata-composition-id="${escapeRegex(compId)}"[^>]*)>`,
221
158
  "i",
222
159
  );
223
160
  const match = pattern.exec(html);
@@ -264,13 +201,8 @@ export function readAttributeByTarget(
264
201
  if (!match) return undefined;
265
202
 
266
203
  const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
267
- const valueMatch = new RegExp(`\\b${fullAttr}=(["'])([^"']*)\\1`).exec(match.tag);
268
- return valueMatch?.[2];
269
- }
270
-
271
- export function readTagSnippetByTarget(html: string, target: PatchTarget): string | undefined {
272
- const match = findTagByTarget(html, target);
273
- return match?.tag;
204
+ const valueMatch = new RegExp(`\\b${fullAttr}="([^"]*)"`).exec(match.tag);
205
+ return valueMatch?.[1];
274
206
  }
275
207
 
276
208
  function patchAttributeByTarget(
@@ -283,7 +215,7 @@ function patchAttributeByTarget(
283
215
  if (!match) return html;
284
216
 
285
217
  const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
286
- const attrPattern = new RegExp(`\\b${fullAttr}=(["'])([^"']*)\\1`);
218
+ const attrPattern = new RegExp(`\\b${fullAttr}="[^"]*"`);
287
219
  const tag = match.tag;
288
220
 
289
221
  if (attrPattern.test(tag)) {
@@ -299,13 +231,13 @@ function patchAttributeByTarget(
299
231
  * Apply an attribute change to an element in the HTML source.
300
232
  */
301
233
  function patchAttribute(html: string, elementId: string, attr: string, value: string): string {
302
- const idPattern = new RegExp(`(<[^>]*\\bid=(["'])${escapeRegex(elementId)}\\2[^>]*)>`, "i");
234
+ const idPattern = new RegExp(`(<[^>]*\\bid="${escapeRegex(elementId)}"[^>]*)>`, "i");
303
235
  const match = idPattern.exec(html);
304
236
  if (!match) return html;
305
237
 
306
238
  const tag = match[1];
307
239
  const fullAttr = attr.startsWith("data-") ? attr : `data-${attr}`;
308
- const attrPattern = new RegExp(`\\b${fullAttr}=(["'])([^"']*)\\1`);
240
+ const attrPattern = new RegExp(`\\b${fullAttr}="[^"]*"`);
309
241
 
310
242
  if (attrPattern.test(tag)) {
311
243
  // Update existing attribute
@@ -322,53 +254,11 @@ function patchAttribute(html: string, elementId: string, attr: string, value: st
322
254
  * Apply a text content change to an element.
323
255
  */
324
256
  function patchTextContent(html: string, elementId: string, value: string): string {
325
- const openTagPattern = new RegExp(
326
- `(<([a-z0-9-]+)[^>]*\\bid=(["'])${escapeRegex(elementId)}\\3[^>]*>)`,
327
- "i",
328
- );
329
- const match = openTagPattern.exec(html);
330
- if (!match || match.index == null) return html;
331
-
332
- const openingTag = match[1];
333
- const tagName = match[2];
334
- const contentStart = match.index + openingTag.length;
335
- const closingIndex = findMatchingClosingTagIndex(html, tagName, contentStart);
336
- if (closingIndex < 0) return html;
337
-
338
- return `${html.slice(0, contentStart)}${value}${html.slice(closingIndex)}`;
339
- }
340
-
341
- function findMatchingClosingTagIndex(html: string, tagName: string, contentStart: number): number {
342
- const tagPattern = new RegExp(`</?${escapeRegex(tagName)}\\b[^>]*>`, "gi");
343
- tagPattern.lastIndex = contentStart;
344
- let depth = 1;
345
- let match: RegExpExecArray | null;
346
-
347
- while ((match = tagPattern.exec(html)) !== null) {
348
- const tag = match[0];
349
- if (tag.startsWith("</")) {
350
- depth -= 1;
351
- if (depth === 0) return match.index;
352
- continue;
353
- }
354
- if (!tag.endsWith("/>")) depth += 1;
355
- }
356
-
357
- return -1;
358
- }
359
-
360
- function patchTextContentByTarget(html: string, target: PatchTarget, value: string): string {
361
- const match = findTagByTarget(html, target);
257
+ // Match the element and its content: <tagname id="elementId"...>content</tagname>
258
+ const pattern = new RegExp(`(<[^>]*\\bid="${elementId}"[^>]*>)([\\s\\S]*?)(<\\/[a-z]+>)`, "i");
259
+ const match = pattern.exec(html);
362
260
  if (!match) return html;
363
-
364
- const tagNameMatch = /^<([a-z0-9-]+)/i.exec(match.tag);
365
- const tagName = tagNameMatch?.[1];
366
- if (!tagName) return html;
367
-
368
- const closingIndex = findMatchingClosingTagIndex(html, tagName, match.end + 1);
369
- if (closingIndex < 0) return html;
370
-
371
- return `${html.slice(0, match.end + 1)}${value}${html.slice(closingIndex)}`;
261
+ return html.replace(pattern, `${match[1]}${value}${match[3]}`);
372
262
  }
373
263
 
374
264
  /**
@@ -400,8 +290,6 @@ export function applyPatchByTarget(html: string, target: PatchTarget, op: PatchO
400
290
  return patchInlineStyleByTarget(html, target, op.property, op.value);
401
291
  case "attribute":
402
292
  return patchAttributeByTarget(html, target, op.property, op.value);
403
- case "text-content":
404
- return patchTextContentByTarget(html, target, op.value);
405
293
  default:
406
294
  return html;
407
295
  }
@@ -4,7 +4,6 @@ import {
4
4
  buildTimelineAssetInsertHtml,
5
5
  getTimelineAssetKind,
6
6
  insertTimelineAssetIntoSource,
7
- resolveTimelineAssetInitialGeometry,
8
7
  resolveTimelineAssetSrc,
9
8
  } from "./timelineAssetDrop";
10
9
 
@@ -20,21 +19,17 @@ describe("getTimelineAssetKind", () => {
20
19
 
21
20
  describe("buildTimelineAssetInsertHtml", () => {
22
21
  it("builds an image clip with explicit timing and track", () => {
23
- const html = buildTimelineAssetInsertHtml({
24
- id: "photo_asset",
25
- assetPath: "assets/photo.png",
26
- kind: "image",
27
- start: 1.25,
28
- duration: 3,
29
- track: 2,
30
- zIndex: 4,
31
- geometry: { left: 0, top: 0, width: 1280, height: 720 },
32
- });
33
-
34
- expect(html).toContain('img id="photo_asset"');
35
- expect(html).toContain("left: 0px");
36
- expect(html).toContain("width: 1280px");
37
- expect(html).not.toContain("inset:");
22
+ expect(
23
+ buildTimelineAssetInsertHtml({
24
+ id: "photo_asset",
25
+ assetPath: "assets/photo.png",
26
+ kind: "image",
27
+ start: 1.25,
28
+ duration: 3,
29
+ track: 2,
30
+ zIndex: 4,
31
+ }),
32
+ ).toContain('img id="photo_asset"');
38
33
  });
39
34
 
40
35
  it("builds an audio clip without visual layout styles", () => {
@@ -52,21 +47,6 @@ describe("buildTimelineAssetInsertHtml", () => {
52
47
  });
53
48
  });
54
49
 
55
- describe("resolveTimelineAssetInitialGeometry", () => {
56
- it("uses the target composition dimensions for visual media", () => {
57
- expect(
58
- resolveTimelineAssetInitialGeometry(
59
- `<div data-composition-id="main" data-width="330" data-height="228"></div>`,
60
- ),
61
- ).toEqual({
62
- left: 0,
63
- top: 0,
64
- width: 330,
65
- height: 228,
66
- });
67
- });
68
- });
69
-
70
50
  describe("resolveTimelineAssetSrc", () => {
71
51
  it("keeps project-root asset paths for index.html", () => {
72
52
  expect(resolveTimelineAssetSrc("index.html", "assets/photo.png")).toBe("assets/photo.png");
@@ -76,23 +76,6 @@ export function buildTimelineFileDropPlacements(
76
76
  });
77
77
  }
78
78
 
79
- export function resolveTimelineAssetInitialGeometry(source: string): {
80
- left: number;
81
- top: number;
82
- width: number;
83
- height: number;
84
- } {
85
- const width = Number.parseFloat(source.match(/\bdata-width=(["'])([^"']+)\1/i)?.[2] ?? "");
86
- const height = Number.parseFloat(source.match(/\bdata-height=(["'])([^"']+)\1/i)?.[2] ?? "");
87
-
88
- return {
89
- left: 0,
90
- top: 0,
91
- width: Number.isFinite(width) && width > 0 ? Math.round(width) : 640,
92
- height: Number.isFinite(height) && height > 0 ? Math.round(height) : 360,
93
- };
94
- }
95
-
96
79
  export function buildTimelineAssetInsertHtml(input: {
97
80
  id: string;
98
81
  assetPath: string;
@@ -101,18 +84,15 @@ export function buildTimelineAssetInsertHtml(input: {
101
84
  duration: number;
102
85
  track: number;
103
86
  zIndex: number;
104
- geometry?: { left: number; top: number; width: number; height: number };
105
87
  }): string {
106
88
  const sharedAttrs = `id="${input.id}" class="clip" src="${input.assetPath}" data-start="${input.start}" data-duration="${input.duration}" data-track-index="${input.track}"`;
107
- const geometry = input.geometry ?? { left: 0, top: 0, width: 640, height: 360 };
108
- const visualStyles = `position: absolute; left: ${geometry.left}px; top: ${geometry.top}px; width: ${geometry.width}px; height: ${geometry.height}px; object-fit: contain; z-index: ${input.zIndex}`;
109
89
 
110
90
  if (input.kind === "image") {
111
- return `<img ${sharedAttrs} style="${visualStyles}" />`;
91
+ return `<img ${sharedAttrs} style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; z-index: ${input.zIndex}" />`;
112
92
  }
113
93
 
114
94
  if (input.kind === "video") {
115
- return `<video ${sharedAttrs} muted playsinline style="${visualStyles}"></video>`;
95
+ return `<video ${sharedAttrs} muted playsinline style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; z-index: ${input.zIndex}"></video>`;
116
96
  }
117
97
 
118
98
  return `<audio ${sharedAttrs} style="z-index: ${input.zIndex}"></audio>`;
@@ -1,198 +0,0 @@
1
- var D=Object.defineProperty;var F=(c,b,e)=>b in c?D(c,b,{enumerable:!0,configurable:!0,writable:!0,value:e}):c[b]=e;var p=(c,b,e)=>F(c,typeof b!="symbol"?b+"":b,e);const N=`
2
- :host {
3
- display: block;
4
- position: relative;
5
- overflow: hidden;
6
- background: #000;
7
- contain: layout style;
8
- }
9
-
10
- .hfp-container {
11
- position: absolute;
12
- inset: 0;
13
- overflow: hidden;
14
- pointer-events: none;
15
- }
16
-
17
-
18
- .hfp-iframe {
19
- position: absolute;
20
- top: 50%;
21
- left: 50%;
22
- border: none;
23
- pointer-events: none;
24
- }
25
-
26
- .hfp-poster {
27
- position: absolute;
28
- inset: 0;
29
- object-fit: contain;
30
- z-index: 1;
31
- pointer-events: none;
32
- }
33
-
34
- /* ── Theming via CSS custom properties ──
35
- *
36
- * Override from outside the shadow DOM:
37
- * hyperframes-player {
38
- * --hfp-controls-bg: linear-gradient(transparent, rgba(0,0,0,0.9));
39
- * --hfp-accent: #ff6b6b;
40
- * --hfp-font: "Inter", sans-serif;
41
- * }
42
- */
43
-
44
- .hfp-controls {
45
- position: absolute;
46
- bottom: 0;
47
- left: 0;
48
- right: 0;
49
- display: flex;
50
- align-items: center;
51
- gap: var(--hfp-controls-gap, 12px);
52
- padding: var(--hfp-controls-padding, 8px 16px);
53
- background: var(--hfp-controls-bg, linear-gradient(transparent, rgba(0, 0, 0, 0.7)));
54
- color: var(--hfp-color, #fff);
55
- font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
56
- font-size: var(--hfp-font-size, 13px);
57
- z-index: 10;
58
- pointer-events: auto;
59
- opacity: 1;
60
- transition: opacity 0.3s ease;
61
- user-select: none;
62
- }
63
-
64
- .hfp-controls.hfp-hidden {
65
- opacity: 0;
66
- pointer-events: none;
67
- }
68
-
69
- .hfp-play-btn {
70
- background: none;
71
- border: none;
72
- color: var(--hfp-color, #fff);
73
- cursor: pointer;
74
- padding: 8px;
75
- display: flex;
76
- align-items: center;
77
- justify-content: center;
78
- width: 40px;
79
- height: 40px;
80
- flex-shrink: 0;
81
- z-index: 10;
82
- }
83
-
84
- .hfp-play-btn:hover {
85
- opacity: 0.8;
86
- }
87
-
88
- .hfp-play-btn svg,
89
- .hfp-play-btn svg * {
90
- pointer-events: none;
91
- }
92
-
93
- .hfp-scrubber {
94
- flex: 1;
95
- height: var(--hfp-scrubber-height, 4px);
96
- background: var(--hfp-scrubber-bg, rgba(255, 255, 255, 0.3));
97
- border-radius: var(--hfp-scrubber-radius, 2px);
98
- cursor: pointer;
99
- position: relative;
100
- }
101
-
102
- .hfp-scrubber:hover {
103
- height: var(--hfp-scrubber-height-hover, 6px);
104
- }
105
-
106
- .hfp-progress {
107
- position: absolute;
108
- top: 0;
109
- left: 0;
110
- height: 100%;
111
- background: var(--hfp-accent, #fff);
112
- border-radius: var(--hfp-scrubber-radius, 2px);
113
- pointer-events: none;
114
- }
115
-
116
- .hfp-time {
117
- flex-shrink: 0;
118
- font-variant-numeric: tabular-nums;
119
- opacity: 0.9;
120
- }
121
-
122
- .hfp-speed-wrap {
123
- position: relative;
124
- flex-shrink: 0;
125
- }
126
-
127
- .hfp-speed-btn {
128
- background: var(--hfp-speed-btn-bg, rgba(255, 255, 255, 0.15));
129
- border: none;
130
- border-radius: var(--hfp-speed-btn-radius, 4px);
131
- color: var(--hfp-color, #fff);
132
- cursor: pointer;
133
- font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
134
- font-size: 12px;
135
- font-variant-numeric: tabular-nums;
136
- font-weight: 600;
137
- padding: 4px 8px;
138
- min-width: 40px;
139
- text-align: center;
140
- transition: background 0.15s ease;
141
- }
142
-
143
- .hfp-speed-btn:hover {
144
- background: var(--hfp-speed-btn-bg-hover, rgba(255, 255, 255, 0.3));
145
- }
146
-
147
- .hfp-speed-menu {
148
- position: absolute;
149
- bottom: calc(100% + 8px);
150
- right: 0;
151
- background: var(--hfp-menu-bg, rgba(20, 20, 20, 0.95));
152
- backdrop-filter: blur(12px);
153
- -webkit-backdrop-filter: blur(12px);
154
- border: 1px solid var(--hfp-menu-border, rgba(255, 255, 255, 0.1));
155
- border-radius: var(--hfp-menu-radius, 8px);
156
- padding: 4px;
157
- display: flex;
158
- flex-direction: column;
159
- gap: 2px;
160
- min-width: 80px;
161
- opacity: 0;
162
- visibility: hidden;
163
- transform: translateY(4px);
164
- transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
165
- box-shadow: var(--hfp-menu-shadow, 0 8px 24px rgba(0, 0, 0, 0.4));
166
- }
167
-
168
- .hfp-speed-menu.hfp-open {
169
- opacity: 1;
170
- visibility: visible;
171
- transform: translateY(0);
172
- }
173
-
174
- .hfp-speed-option {
175
- background: none;
176
- border: none;
177
- border-radius: 4px;
178
- color: var(--hfp-menu-color, rgba(255, 255, 255, 0.7));
179
- cursor: pointer;
180
- font-family: var(--hfp-font, system-ui, -apple-system, sans-serif);
181
- font-size: 13px;
182
- font-variant-numeric: tabular-nums;
183
- padding: 6px 12px;
184
- text-align: left;
185
- transition: background 0.1s ease, color 0.1s ease;
186
- white-space: nowrap;
187
- }
188
-
189
- .hfp-speed-option:hover {
190
- background: var(--hfp-menu-hover-bg, rgba(255, 255, 255, 0.1));
191
- color: var(--hfp-color, #fff);
192
- }
193
-
194
- .hfp-speed-option.hfp-active {
195
- color: var(--hfp-accent, #fff);
196
- font-weight: 600;
197
- }
198
- `,O='<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><polygon points="4,2 16,9 4,16"/></svg>',U='<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><rect x="3" y="2" width="4" height="14"/><rect x="11" y="2" width="4" height="14"/></svg>',j=[.25,.5,1,1.5,2,4];function k(c){return Number.isInteger(c)?`${c}x`:`${c}x`}function R(c){if(!Number.isFinite(c)||c<0)return"0:00";const b=Math.floor(c),e=Math.floor(b/60),t=b%60;return`${e}:${t.toString().padStart(2,"0")}`}function z(c,b,e={}){const t=e.speedPresets??j,s=document.createElement("div");s.className="hfp-controls",s.addEventListener("click",o=>{o.stopPropagation()});const i=document.createElement("button");i.className="hfp-play-btn",i.type="button",i.innerHTML=O,i.setAttribute("aria-label","Play");const r=document.createElement("div");r.className="hfp-scrubber";const n=document.createElement("div");n.className="hfp-progress",n.style.width="0%",r.appendChild(n);const d=document.createElement("span");d.className="hfp-time",d.textContent="0:00 / 0:00";const _=document.createElement("div");_.className="hfp-speed-wrap";const f=document.createElement("button");f.className="hfp-speed-btn",f.type="button",f.textContent="1x",f.setAttribute("aria-label","Playback speed");const a=document.createElement("div");a.className="hfp-speed-menu",a.setAttribute("role","menu");for(const o of t){const h=document.createElement("button");h.className="hfp-speed-option",h.type="button",h.setAttribute("role","menuitem"),h.dataset.speed=String(o),h.textContent=k(o),o===1&&h.classList.add("hfp-active"),a.appendChild(h)}_.appendChild(a),_.appendChild(f),s.appendChild(i),s.appendChild(r),s.appendChild(d),s.appendChild(_),c.appendChild(s);let l=!1,u=null;t.indexOf(1),i.addEventListener("click",o=>{o.stopPropagation(),l?b.onPause():b.onPlay()});const m=o=>{for(const h of a.querySelectorAll(".hfp-speed-option"))h.classList.toggle("hfp-active",h.dataset.speed===String(o))};f.addEventListener("click",o=>{o.stopPropagation();const h=a.classList.toggle("hfp-open");f.setAttribute("aria-expanded",String(h))}),a.addEventListener("click",o=>{o.stopPropagation();const h=o.target.closest(".hfp-speed-option");if(!h)return;const y=parseFloat(h.dataset.speed);t.indexOf(y),f.textContent=k(y),m(y),a.classList.remove("hfp-open"),f.setAttribute("aria-expanded","false"),b.onSpeedChange(y)});const v=()=>{a.classList.remove("hfp-open"),f.setAttribute("aria-expanded","false")};document.addEventListener("click",v);const E=o=>{const h=r.getBoundingClientRect(),y=Math.max(0,Math.min(1,(o-h.left)/h.width));b.onSeek(y)};let g=!1;r.addEventListener("mousedown",o=>{o.stopPropagation(),g=!0,E(o.clientX)});const A=o=>{g&&E(o.clientX)},C=()=>{g=!1};document.addEventListener("mousemove",A),document.addEventListener("mouseup",C),r.addEventListener("touchstart",o=>{g=!0;const h=o.touches[0];h&&E(h.clientX)},{passive:!0});const P=o=>{if(g){const h=o.touches[0];h&&E(h.clientX)}},I=()=>{g=!1};document.addEventListener("touchmove",P,{passive:!0}),document.addEventListener("touchend",I);const T=()=>{u&&clearTimeout(u),u=setTimeout(()=>{l&&s.classList.add("hfp-hidden")},3e3)},L=c instanceof ShadowRoot?c.host:c;return L.addEventListener("mousemove",()=>{s.classList.remove("hfp-hidden"),T()}),L.addEventListener("mouseleave",()=>{l&&s.classList.add("hfp-hidden")}),{updateTime(o,h){const y=h>0?o/h*100:0;n.style.width=`${y}%`,d.textContent=`${R(o)} / ${R(h)}`},updatePlaying(o){l=o,i.innerHTML=o?U:O,i.setAttribute("aria-label",o?"Pause":"Play"),o?T():s.classList.remove("hfp-hidden")},updateSpeed(o){t.indexOf(o),f.textContent=k(o),m(o)},show(){s.style.display=""},hide(){s.style.display="none"},destroy(){document.removeEventListener("mousemove",A),document.removeEventListener("mouseup",C),document.removeEventListener("touchmove",P),document.removeEventListener("touchend",I),document.removeEventListener("click",v),u&&clearTimeout(u)}}}function H(c){return c.hasRuntime||c.runtimeInjected?!1:!!(c.hasNestedCompositions||c.hasTimelines&&c.attempts>=5)}let M=null;function q(){if(M)return M;if(typeof CSSStyleSheet>"u")return null;try{const c=new CSSStyleSheet;return c.replaceSync(N),M=c,c}catch{return null}}const S=30,W="https://cdn.jsdelivr.net/npm/@hyperframes/core/dist/hyperframe.runtime.iife.js",w=class w extends HTMLElement{constructor(){super();p(this,"shadow");p(this,"container");p(this,"iframe");p(this,"posterEl",null);p(this,"controlsApi",null);p(this,"resizeObserver");p(this,"_ready",!1);p(this,"_duration",0);p(this,"_currentTime",0);p(this,"_paused",!0);p(this,"_compositionWidth",1920);p(this,"_compositionHeight",1080);p(this,"_probeInterval",null);p(this,"_lastUpdateMs",0);p(this,"_parentMedia",[]);p(this,"_audioOwner","runtime");p(this,"_mediaObserver");p(this,"_playbackErrorPosted",!1);p(this,"_runtimeInjected",!1);this.shadow=this.attachShadow({mode:"open"});const e=q();if(e)this.shadow.adoptedStyleSheets=[e];else{const t=document.createElement("style");t.textContent=N,this.shadow.appendChild(t)}this.container=document.createElement("div"),this.container.className="hfp-container",this.iframe=document.createElement("iframe"),this.iframe.className="hfp-iframe",this.iframe.sandbox.add("allow-scripts","allow-same-origin"),this.iframe.allow="autoplay; fullscreen",this.iframe.referrerPolicy="no-referrer",this.iframe.title="HyperFrames Composition",this.container.appendChild(this.iframe),this.shadow.appendChild(this.container),this.addEventListener("click",t=>{this._isControlsClick(t)||(this._paused?this.play():this.pause())}),this.resizeObserver=new ResizeObserver(()=>this._updateScale()),this._onMessage=this._onMessage.bind(this),this._onIframeLoad=this._onIframeLoad.bind(this)}static get observedAttributes(){return["src","srcdoc","width","height","controls","muted","poster","playback-rate","audio-src"]}connectedCallback(){this.resizeObserver.observe(this),window.addEventListener("message",this._onMessage),this.iframe.addEventListener("load",this._onIframeLoad),this.hasAttribute("controls")&&this._setupControls(),this.hasAttribute("poster")&&this._setupPoster(),this.hasAttribute("audio-src")&&this._setupParentAudioFromUrl(this.getAttribute("audio-src")),this.hasAttribute("srcdoc")&&(this.iframe.srcdoc=this.getAttribute("srcdoc")),this.hasAttribute("src")&&(this.iframe.src=this.getAttribute("src"))}disconnectedCallback(){var e;this.resizeObserver.disconnect(),window.removeEventListener("message",this._onMessage),this.iframe.removeEventListener("load",this._onIframeLoad),this._probeInterval&&clearInterval(this._probeInterval),this._teardownMediaObserver(),(e=this.controlsApi)==null||e.destroy();for(const t of this._parentMedia)t.el.pause(),t.el.src="";this._parentMedia=[]}attributeChangedCallback(e,t,s){var i,r;switch(e){case"src":s&&(this._ready=!1,this.iframe.src=s);break;case"srcdoc":this._ready=!1,s!==null?this.iframe.srcdoc=s:this.iframe.removeAttribute("srcdoc");break;case"width":this._compositionWidth=parseInt(s||"1920",10),this._updateScale();break;case"height":this._compositionHeight=parseInt(s||"1080",10),this._updateScale();break;case"controls":s!==null?this._setupControls():((i=this.controlsApi)==null||i.destroy(),this.controlsApi=null);break;case"poster":this._setupPoster();break;case"playback-rate":{const n=parseFloat(s||"1");for(const d of this._parentMedia)d.el.playbackRate=n;this._sendControl("set-playback-rate",{playbackRate:n}),(r=this.controlsApi)==null||r.updateSpeed(n),this.dispatchEvent(new Event("ratechange"));break}case"muted":for(const n of this._parentMedia)n.el.muted=s!==null;this._sendControl("set-muted",{muted:s!==null});break;case"audio-src":s&&this._setupParentAudioFromUrl(s);break}}get iframeElement(){return this.iframe}play(){var e;this._hidePoster(),this._sendControl("play"),this._audioOwner==="parent"&&this._playParentMedia(),this._paused=!1,(e=this.controlsApi)==null||e.updatePlaying(!0),this.dispatchEvent(new Event("play"))}pause(){var e;this._sendControl("pause"),this._audioOwner==="parent"&&this._pauseParentMedia(),this._paused=!0,(e=this.controlsApi)==null||e.updatePlaying(!1),this.dispatchEvent(new Event("pause"))}seek(e){var t,s;if(!this._trySyncSeek(e)){const i=Math.round(e*S);this._sendControl("seek",{frame:i})}if(this._currentTime=e,this._audioOwner==="parent")for(const i of this._parentMedia){const r=e-i.start;r>=0&&r<i.duration&&(i.el.currentTime=r)}this._paused=!0,(t=this.controlsApi)==null||t.updatePlaying(!1),(s=this.controlsApi)==null||s.updateTime(this._currentTime,this._duration)}get currentTime(){return this._currentTime}set currentTime(e){this.seek(e)}get duration(){return this._duration}get paused(){return this._paused}get ready(){return this._ready}get playbackRate(){return parseFloat(this.getAttribute("playback-rate")||"1")}set playbackRate(e){this.setAttribute("playback-rate",String(e))}get muted(){return this.hasAttribute("muted")}set muted(e){e?this.setAttribute("muted",""):this.removeAttribute("muted")}get loop(){return this.hasAttribute("loop")}set loop(e){e?this.setAttribute("loop",""):this.removeAttribute("loop")}_sendControl(e,t={}){var s;try{(s=this.iframe.contentWindow)==null||s.postMessage({source:"hf-parent",type:"control",action:e,...t},"*")}catch{}}_trySyncSeek(e){try{const t=this.iframe.contentWindow,s=t==null?void 0:t.__player,i=s==null?void 0:s.seek;return typeof i!="function"?!1:(i.call(s,e),!0)}catch{return!1}}_isControlsClick(e){return e.composedPath().some(t=>t instanceof HTMLElement&&t.classList.contains("hfp-controls"))}_onMessage(e){var s,i,r,n;if(e.source!==this.iframe.contentWindow)return;const t=e.data;if(!(!t||t.source!=="hf-preview")){if(t.type==="state"){this._currentTime=(t.frame??0)/S;const d=!this._paused;this._paused=!t.isPlaying,this._audioOwner==="parent"&&(d&&this._paused?this._pauseParentMedia():!d&&!this._paused&&this._playParentMedia(),this._mirrorParentMediaTime(this._currentTime));const _=performance.now();(_-this._lastUpdateMs>100||this._paused!==d)&&(this._lastUpdateMs=_,(s=this.controlsApi)==null||s.updateTime(this._currentTime,this._duration),(i=this.controlsApi)==null||i.updatePlaying(!this._paused),this.dispatchEvent(new CustomEvent("timeupdate",{detail:{currentTime:this._currentTime}}))),this._currentTime>=this._duration&&!this._paused&&(this._audioOwner==="parent"&&this._pauseParentMedia(),this.loop?(this.seek(0),this.play()):(this._paused=!0,(r=this.controlsApi)==null||r.updatePlaying(!1),this.dispatchEvent(new Event("ended"))))}t.type==="media-autoplay-blocked"&&this._promoteToParentProxy(),t.type==="timeline"&&t.durationInFrames>0&&Number.isFinite(t.durationInFrames)&&(this._duration=t.durationInFrames/S,(n=this.controlsApi)==null||n.updateTime(this._currentTime,this._duration)),t.type==="stage-size"&&t.width>0&&t.height>0&&(this._compositionWidth=t.width,this._compositionHeight=t.height,this._updateScale())}}_onIframeLoad(){let e=0;this._runtimeInjected=!1;const t=this._audioOwner==="parent";this._audioOwner="runtime",this._playbackErrorPosted=!1,this._pauseParentMedia(),this._teardownMediaObserver(),t&&this.dispatchEvent(new CustomEvent("audioownershipchange",{detail:{owner:"runtime",reason:"iframe-reload"}})),this._probeInterval&&clearInterval(this._probeInterval),this._probeInterval=setInterval(()=>{var s,i;e++;try{const r=this.iframe.contentWindow;if(!r)return;const n=!!(r.__hf||r.__player),d=!!(r.__timelines&&Object.keys(r.__timelines).length>0),_=!!((s=this.iframe.contentDocument)!=null&&s.querySelector("[data-composition-src]"));if(H({hasRuntime:n,hasTimelines:d,hasNestedCompositions:_,runtimeInjected:this._runtimeInjected,attempts:e})){this._injectRuntime();return}if(this._runtimeInjected&&!n)return;const a=(()=>{var l,u;if(r.__player&&typeof r.__player.getDuration=="function")return r.__player;if(r.__timelines){const m=Object.keys(r.__timelines);if(m.length>0){const v=(u=(l=this.iframe.contentDocument)==null?void 0:l.querySelector("[data-composition-id]"))==null?void 0:u.getAttribute("data-composition-id"),E=v&&v in r.__timelines?v:m[m.length-1],g=r.__timelines[E];return{getDuration:()=>g.duration()}}}return null})();if(a&&a.getDuration()>0){clearInterval(this._probeInterval),this._duration=a.getDuration(),this._ready=!0,(i=this.controlsApi)==null||i.updateTime(0,this._duration),this.dispatchEvent(new CustomEvent("ready",{detail:{duration:this._duration}}));const l=this.iframe.contentDocument,u=l==null?void 0:l.querySelector("[data-composition-id]");if(u){const m=parseInt(u.getAttribute("data-width")||"0",10),v=parseInt(u.getAttribute("data-height")||"0",10);m>0&&v>0&&(this._compositionWidth=m,this._compositionHeight=v,this._updateScale())}this._setupParentMedia(),this.hasAttribute("autoplay")&&this.play();return}}catch{}e>=40&&(clearInterval(this._probeInterval),this.dispatchEvent(new CustomEvent("error",{detail:{message:"Composition timeline not found after 8s"}})))},200)}_injectRuntime(){this._runtimeInjected=!0;try{const e=this.iframe.contentDocument;if(!e)return;const t=e.createElement("script");t.src=W,t.onload=()=>{},t.onerror=()=>{},(e.head||e.documentElement).appendChild(t)}catch{}}_updateScale(){const e=this.getBoundingClientRect();if(e.width===0||e.height===0)return;const t=Math.min(e.width/this._compositionWidth,e.height/this._compositionHeight);this.iframe.style.width=`${this._compositionWidth}px`,this.iframe.style.height=`${this._compositionHeight}px`,this.iframe.style.transform=`translate(-50%, -50%) scale(${t})`}_setupControls(){if(this.controlsApi)return;const e={onPlay:()=>this.play(),onPause:()=>this.pause(),onSeek:i=>this.seek(i*this._duration),onSpeedChange:i=>{this.playbackRate=i}},t=this.getAttribute("speed-presets"),s=t?t.split(",").map(Number).filter(i=>!isNaN(i)&&i>0):void 0;this.controlsApi=z(this.shadow,e,{speedPresets:s})}_setupPoster(){var t;const e=this.getAttribute("poster");if(!e){(t=this.posterEl)==null||t.remove(),this.posterEl=null;return}this.posterEl||(this.posterEl=document.createElement("img"),this.posterEl.className="hfp-poster",this.shadow.appendChild(this.posterEl)),this.posterEl.src=e}_playParentMedia(){for(const e of this._parentMedia)e.el.src&&e.el.play().catch(t=>this._reportPlaybackError(t))}_reportPlaybackError(e){this._playbackErrorPosted||(this._playbackErrorPosted=!0,this.dispatchEvent(new CustomEvent("playbackerror",{detail:{source:"parent-proxy",error:e}})))}_pauseParentMedia(){for(const e of this._parentMedia)e.el.pause()}_mirrorParentMediaTime(e,t){const s=(t==null?void 0:t.force)===!0,i=w.MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES,r=w.MIRROR_DRIFT_THRESHOLD_SECONDS;for(const n of this._parentMedia){const d=e-n.start;if(d<0||d>=n.duration){n.driftSamples=0;continue}Math.abs(n.el.currentTime-d)>r?(n.driftSamples+=1,(s||n.driftSamples>=i)&&(n.el.currentTime=d,n.driftSamples=0)):n.driftSamples=0}}_promoteToParentProxy(){this._audioOwner!=="parent"&&(this._audioOwner="parent",this._sendControl("set-media-output-muted",{muted:!0}),this._mirrorParentMediaTime(this._currentTime,{force:!0}),this._paused||this._playParentMedia(),this.dispatchEvent(new CustomEvent("audioownershipchange",{detail:{owner:"parent",reason:"autoplay-blocked"}})))}_createParentMedia(e,t,s,i){if(this._parentMedia.some(d=>d.el.src===e))return null;const r=t==="video"?document.createElement("video"):new Audio;r.preload="auto",r.src=e,r.load(),r.muted=this.muted,this.playbackRate!==1&&(r.playbackRate=this.playbackRate);const n={el:r,start:s,duration:i,driftSamples:0};return this._parentMedia.push(n),n}_setupParentAudioFromUrl(e){this._createParentMedia(e,"audio",0,1/0)}_setupParentMedia(){try{const e=this.iframe.contentDocument;if(!e)return;const t=e.querySelectorAll("audio[data-start], video[data-start]");for(const s of t)this._adoptIframeMedia(s);this._observeDynamicMedia(e)}catch{}}_adoptIframeMedia(e){var _;const t=e.getAttribute("src")||((_=e.querySelector("source"))==null?void 0:_.getAttribute("src"));if(!t)return;const s=new URL(t,e.ownerDocument.baseURI).href,i=parseFloat(e.getAttribute("data-start")||"0"),r=parseFloat(e.getAttribute("data-duration")||"Infinity"),n=e.tagName==="VIDEO"?"video":"audio",d=this._createParentMedia(s,n,i,r);d&&this._audioOwner==="parent"&&(this._mirrorParentMediaTime(this._currentTime,{force:!0}),!this._paused&&d.el.src&&d.el.play().catch(f=>this._reportPlaybackError(f)))}_observeDynamicMedia(e){if(this._teardownMediaObserver(),typeof MutationObserver>"u"||!e.body)return;const t=new MutationObserver(i=>{var r,n,d,_;for(const f of i){for(const a of f.addedNodes){if(!(a instanceof Element))continue;const l=[];(r=a.matches)!=null&&r.call(a,"audio[data-start], video[data-start]")&&l.push(a);const u=(n=a.querySelectorAll)==null?void 0:n.call(a,"audio[data-start], video[data-start]");if(u)for(const m of u)l.push(m);for(const m of l)this._adoptIframeMedia(m)}for(const a of f.removedNodes){if(!(a instanceof Element))continue;const l=[];(d=a.matches)!=null&&d.call(a,"audio[data-start], video[data-start]")&&l.push(a);const u=(_=a.querySelectorAll)==null?void 0:_.call(a,"audio[data-start], video[data-start]");if(u)for(const m of u)l.push(m);for(const m of l)this._detachIframeMedia(m)}}}),s=e.querySelectorAll("[data-composition-id]");if(s.length>0)for(const i of s)t.observe(i,{childList:!0,subtree:!0});else t.observe(e.body,{childList:!0,subtree:!0});this._mediaObserver=t}_teardownMediaObserver(){var e;(e=this._mediaObserver)==null||e.disconnect(),this._mediaObserver=void 0}_detachIframeMedia(e){var n;const t=e.getAttribute("src")||((n=e.querySelector("source"))==null?void 0:n.getAttribute("src"));if(!t)return;const s=new URL(t,e.ownerDocument.baseURI).href,i=this._parentMedia.findIndex(d=>d.el.src===s);if(i===-1)return;const r=this._parentMedia[i];r.el.pause(),r.el.src="",this._parentMedia.splice(i,1)}_hidePoster(){var e;(e=this.posterEl)==null||e.remove(),this.posterEl=null}};p(w,"MIRROR_DRIFT_THRESHOLD_SECONDS",.05),p(w,"MIRROR_REQUIRED_CONSECUTIVE_DRIFT_SAMPLES",2);let x=w;customElements.get("hyperframes-player")||customElements.define("hyperframes-player",x);export{x as HyperframesPlayer,j as SPEED_PRESETS,k as formatSpeed,R as formatTime};