@hyperframes/studio 0.6.0 → 0.6.2

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 (58) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-hYc4aP7M.js +117 -0
  3. package/dist/index.html +1 -1
  4. package/package.json +4 -4
  5. package/src/App.tsx +2 -13
  6. package/src/captions/components/CaptionOverlay.tsx +13 -246
  7. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  8. package/src/components/StudioPreviewArea.tsx +6 -2
  9. package/src/components/editor/DomEditOverlay.tsx +88 -1007
  10. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  11. package/src/components/editor/FileTree.tsx +13 -621
  12. package/src/components/editor/FileTreeIcons.tsx +128 -0
  13. package/src/components/editor/FileTreeNodes.tsx +496 -0
  14. package/src/components/editor/MotionPanel.tsx +16 -390
  15. package/src/components/editor/MotionPanelFields.tsx +185 -0
  16. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  17. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  18. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  19. package/src/components/editor/domEditing.ts +44 -1150
  20. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  21. package/src/components/editor/domEditingDom.ts +266 -0
  22. package/src/components/editor/domEditingElement.ts +329 -0
  23. package/src/components/editor/domEditingLayers.ts +460 -0
  24. package/src/components/editor/domEditingTypes.ts +125 -0
  25. package/src/components/editor/manualEdits.ts +84 -1081
  26. package/src/components/editor/manualEditsDom.ts +436 -0
  27. package/src/components/editor/manualEditsParsing.ts +280 -0
  28. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  29. package/src/components/editor/manualEditsTypes.ts +141 -0
  30. package/src/components/editor/studioMotion.ts +47 -434
  31. package/src/components/editor/studioMotionOps.ts +299 -0
  32. package/src/components/editor/studioMotionTypes.ts +168 -0
  33. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  34. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  35. package/src/components/nle/NLELayout.tsx +60 -144
  36. package/src/components/nle/useCompositionStack.ts +126 -0
  37. package/src/hooks/useToast.ts +20 -0
  38. package/src/player/components/Timeline.tsx +189 -1418
  39. package/src/player/components/TimelineCanvas.tsx +434 -0
  40. package/src/player/components/TimelineEmptyState.tsx +102 -0
  41. package/src/player/components/TimelineRuler.tsx +90 -0
  42. package/src/player/components/timelineIcons.tsx +49 -0
  43. package/src/player/components/timelineLayout.ts +215 -0
  44. package/src/player/components/timelineUtils.ts +211 -0
  45. package/src/player/components/useTimelineClipDrag.ts +388 -0
  46. package/src/player/components/useTimelinePlayhead.ts +200 -0
  47. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  48. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  49. package/src/player/hooks/useTimelinePlayer.ts +69 -1372
  50. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  51. package/src/player/lib/playbackAdapter.ts +145 -0
  52. package/src/player/lib/playbackShortcuts.ts +68 -0
  53. package/src/player/lib/playbackTypes.ts +60 -0
  54. package/src/player/lib/timelineDOM.ts +373 -0
  55. package/src/player/lib/timelineElementHelpers.ts +303 -0
  56. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  57. package/dist/assets/hyperframes-player-DOFETgjy.js +0 -418
  58. package/dist/assets/index-DUqUmaoH.js +0 -117
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Runtime iframe integration utilities.
3
+ *
4
+ * Handles the boundary between the studio host page and the preview iframe:
5
+ * - Viewport normalisation on load
6
+ * - Auto-healing missing data-composition-id attributes
7
+ * - Unmuting media via postMessage
8
+ * - Resolving the underlying <iframe> from any wrapper element
9
+ * - Scanning the DOM for composition hosts the manifest missed
10
+ * (element-reference starts that the CDN runtime fails to resolve)
11
+ */
12
+
13
+ import type { TimelineElement } from "../store/playerStore";
14
+ import type { IframeWindow } from "./playbackTypes";
15
+ import {
16
+ getTimelineElementSelector,
17
+ getTimelineElementSourceFile,
18
+ getTimelineElementSelectorIndex,
19
+ getTimelineElementDisplayLabel,
20
+ buildTimelineElementIdentity,
21
+ } from "./timelineElementHelpers";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Viewport / DOM normalisation
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export function normalizePreviewViewport(doc: Document, win: Window): void {
28
+ if (doc.documentElement) {
29
+ doc.documentElement.style.overflow = "hidden";
30
+ doc.documentElement.style.margin = "0";
31
+ }
32
+ if (doc.body) {
33
+ doc.body.style.overflow = "hidden";
34
+ doc.body.style.margin = "0";
35
+ }
36
+ win.scrollTo({ top: 0, left: 0, behavior: "auto" });
37
+ }
38
+
39
+ export function autoHealMissingCompositionIds(doc: Document): void {
40
+ const compositionIdRe = /data-composition-id=["']([^"']+)["']/gi;
41
+ const referencedIds = new Set<string>();
42
+ const scopedNodes = Array.from(doc.querySelectorAll("style, script"));
43
+ for (const node of scopedNodes) {
44
+ const text = node.textContent || "";
45
+ if (!text) continue;
46
+ let match: RegExpExecArray | null;
47
+ while ((match = compositionIdRe.exec(text)) !== null) {
48
+ const id = (match[1] || "").trim();
49
+ if (id) referencedIds.add(id);
50
+ }
51
+ }
52
+
53
+ if (referencedIds.size === 0) return;
54
+
55
+ const existingIds = new Set<string>();
56
+ const existingNodes = Array.from(doc.querySelectorAll<HTMLElement>("[data-composition-id]"));
57
+ for (const node of existingNodes) {
58
+ const id = node.getAttribute("data-composition-id");
59
+ if (id) existingIds.add(id);
60
+ }
61
+
62
+ for (const compId of referencedIds) {
63
+ if (compId === "root" || existingIds.has(compId)) continue;
64
+ const host =
65
+ doc.getElementById(`${compId}-layer`) ||
66
+ doc.getElementById(`${compId}-comp`) ||
67
+ doc.getElementById(compId);
68
+ if (!host) continue;
69
+ if (!host.getAttribute("data-composition-id")) {
70
+ host.setAttribute("data-composition-id", compId);
71
+ }
72
+ }
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Muting / iframe resolution
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export function unmutePreviewMedia(iframe: HTMLIFrameElement | null): void {
80
+ if (!iframe) return;
81
+ try {
82
+ iframe.contentWindow?.postMessage(
83
+ { source: "hf-parent", type: "control", action: "set-muted", muted: false },
84
+ "*",
85
+ );
86
+ } catch (err) {
87
+ console.warn("[useTimelinePlayer] Failed to unmute preview media", err);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Resolve the underlying iframe from any host element. Supports:
93
+ * - Direct `<iframe>` element (most common — studio's own `Player.tsx`)
94
+ * - Custom elements (e.g. `<hyperframes-player>`) whose shadow DOM contains an iframe
95
+ * - Wrapper elements whose light DOM contains a descendant iframe
96
+ *
97
+ * Exported so web-component consumers can pre-resolve the iframe before
98
+ * assigning it to `iframeRef` returned by `useTimelinePlayer`. Returns `null`
99
+ * when the element has no associated iframe yet.
100
+ *
101
+ * @example
102
+ * ```tsx
103
+ * const { iframeRef } = useTimelinePlayer();
104
+ * const playerElRef = useRef<HyperframesPlayer>(null);
105
+ *
106
+ * useEffect(() => {
107
+ * iframeRef.current = resolveIframe(playerElRef.current);
108
+ * }, [iframeRef]);
109
+ * ```
110
+ */
111
+ export function resolveIframe(el: Element | null): HTMLIFrameElement | null {
112
+ if (!el) return null;
113
+ if (el instanceof HTMLIFrameElement) return el;
114
+ return el.shadowRoot?.querySelector("iframe") ?? el.querySelector("iframe") ?? null;
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Enrich missing compositions from DOM
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Scan the iframe DOM for composition hosts missing from the current
123
+ * timeline elements and add them. The CDN runtime often fails to resolve
124
+ * element-reference starts (`data-start="intro"`) so composition hosts
125
+ * are silently dropped from `__clipManifest`. This pass reads the DOM +
126
+ * GSAP timeline registry directly to fill the gaps.
127
+ */
128
+ export function buildMissingCompositionElements(
129
+ doc: Document,
130
+ iframeWin: IframeWindow,
131
+ currentEls: readonly TimelineElement[],
132
+ rootDuration: number,
133
+ ): { missing: TimelineElement[]; updatedEls: TimelineElement[]; patched: boolean } {
134
+ const existingIds = new Set(currentEls.map((e) => e.id));
135
+ const rootComp = doc.querySelector("[data-composition-id]");
136
+ const rootCompId = rootComp?.getAttribute("data-composition-id");
137
+ // Use [data-composition-id][data-start] — the composition loader strips
138
+ // data-composition-src after loading, so we can't rely on it.
139
+ const hosts = doc.querySelectorAll("[data-composition-id][data-start]");
140
+ const missing: TimelineElement[] = [];
141
+
142
+ hosts.forEach((host) => {
143
+ const el = host as HTMLElement;
144
+ const compId = el.getAttribute("data-composition-id");
145
+ if (!compId || compId === rootCompId) return;
146
+ if (existingIds.has(el.id) || existingIds.has(compId)) return;
147
+
148
+ // Resolve start: numeric or element-reference
149
+ const startAttr = el.getAttribute("data-start") ?? "0";
150
+ let start = parseFloat(startAttr);
151
+ if (isNaN(start)) {
152
+ const ref =
153
+ doc.getElementById(startAttr) || doc.querySelector(`[data-composition-id="${startAttr}"]`);
154
+ if (ref) {
155
+ const refStartAttr = ref.getAttribute("data-start") ?? "0";
156
+ let refStart = parseFloat(refStartAttr);
157
+ // Recursively resolve one level of reference for the ref's own start
158
+ if (isNaN(refStart)) {
159
+ const refRef =
160
+ doc.getElementById(refStartAttr) ||
161
+ doc.querySelector(`[data-composition-id="${refStartAttr}"]`);
162
+ const rrStart = parseFloat(refRef?.getAttribute("data-start") ?? "0") || 0;
163
+ const rrCompId = refRef?.getAttribute("data-composition-id");
164
+ const rrDur =
165
+ parseFloat(refRef?.getAttribute("data-duration") ?? "") ||
166
+ (rrCompId
167
+ ? ((
168
+ iframeWin.__timelines?.[rrCompId] as { duration?: () => number } | undefined
169
+ )?.duration?.() ?? 0)
170
+ : 0);
171
+ refStart = rrStart + rrDur;
172
+ }
173
+ const refCompId = ref.getAttribute("data-composition-id");
174
+ const refDur =
175
+ parseFloat(ref.getAttribute("data-duration") ?? "") ||
176
+ (refCompId
177
+ ? ((
178
+ iframeWin.__timelines?.[refCompId] as { duration?: () => number } | undefined
179
+ )?.duration?.() ?? 0)
180
+ : 0);
181
+ start = refStart + refDur;
182
+ } else {
183
+ start = 0;
184
+ }
185
+ }
186
+
187
+ // Resolve duration from data-duration or GSAP timeline
188
+ let dur = parseFloat(el.getAttribute("data-duration") ?? "");
189
+ if (isNaN(dur) || dur <= 0) {
190
+ dur =
191
+ (
192
+ iframeWin.__timelines?.[compId] as { duration?: () => number } | undefined
193
+ )?.duration?.() ?? 0;
194
+ }
195
+ if (!Number.isFinite(dur) || dur <= 0) return;
196
+ if (!Number.isFinite(start)) start = 0;
197
+ if (Number.isFinite(rootDuration) && rootDuration > 0) {
198
+ if (start >= rootDuration) return;
199
+ dur = Math.min(dur, Math.max(0, rootDuration - start));
200
+ if (dur <= 0) return;
201
+ }
202
+
203
+ const trackStr = el.getAttribute("data-track-index");
204
+ const track = trackStr != null ? parseInt(trackStr, 10) : 0;
205
+ const compSrc =
206
+ el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
207
+ const selector = getTimelineElementSelector(el);
208
+ const sourceFile = getTimelineElementSourceFile(el);
209
+ const selectorIndex = getTimelineElementSelectorIndex(doc, el, selector);
210
+ const label = getTimelineElementDisplayLabel({
211
+ id: el.id || compId || null,
212
+ label: el.getAttribute("data-timeline-label") ?? el.getAttribute("data-label"),
213
+ tag: el.tagName,
214
+ });
215
+ const identity = buildTimelineElementIdentity({
216
+ preferredId: el.id || compId || null,
217
+ label,
218
+ fallbackIndex: missing.length,
219
+ domId: el.id || undefined,
220
+ selector,
221
+ selectorIndex,
222
+ sourceFile,
223
+ });
224
+ const entry: TimelineElement = {
225
+ id: identity.id,
226
+ label,
227
+ key: identity.key,
228
+ tag: el.tagName.toLowerCase(),
229
+ start,
230
+ duration: dur,
231
+ track: isNaN(track) ? 0 : track,
232
+ domId: el.id || undefined,
233
+ selector,
234
+ selectorIndex,
235
+ sourceFile,
236
+ };
237
+ if (compSrc) {
238
+ entry.compositionSrc = compSrc;
239
+ } else {
240
+ // Inline composition — expose inner video for thumbnails
241
+ const innerVideo = el.querySelector("video[src]");
242
+ if (innerVideo) {
243
+ entry.src = innerVideo.getAttribute("src") || undefined;
244
+ entry.tag = "video";
245
+ }
246
+ }
247
+ missing.push(entry);
248
+ });
249
+
250
+ // Patch existing elements that are missing compositionSrc
251
+ let patched = false;
252
+ const updatedEls = (currentEls as TimelineElement[]).map((existing) => {
253
+ if (existing.compositionSrc) return existing;
254
+ // Find the matching DOM host by element id or composition id
255
+ const host =
256
+ doc.getElementById(existing.id) ??
257
+ doc.querySelector(`[data-composition-id="${existing.id}"]`);
258
+ if (!host) return existing;
259
+ const compSrc =
260
+ host.getAttribute("data-composition-src") || host.getAttribute("data-composition-file");
261
+ if (compSrc) {
262
+ patched = true;
263
+ return { ...existing, compositionSrc: compSrc };
264
+ }
265
+ return existing;
266
+ });
267
+
268
+ return { missing, updatedEls, patched };
269
+ }