@hyperframes/studio 0.7.60 → 0.7.61

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 (47) hide show
  1. package/dist/assets/hyperframes-player-Xvx2hkrc.js +459 -0
  2. package/dist/assets/{index-cH6NfVV_.js → index-2mxh_HSy.js} +177 -177
  3. package/dist/assets/{index-D6etaey-.js → index-BCpoiv9S.js} +1 -1
  4. package/dist/assets/index-BhWig0mx.css +1 -0
  5. package/dist/assets/{index-Dh_WhagG.js → index-D-uFclFj.js} +1 -1
  6. package/dist/index.html +2 -2
  7. package/dist/index.js +1202 -673
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +3 -8
  11. package/src/components/DesignPanelPromoteProvider.tsx +27 -1
  12. package/src/components/StudioHeader.tsx +2 -3
  13. package/src/components/StudioRightPanel.tsx +34 -26
  14. package/src/components/editor/PromotableControl.tsx +4 -2
  15. package/src/components/editor/PropertyPanel.tsx +151 -149
  16. package/src/components/editor/PropertyPanelFlat.tsx +44 -42
  17. package/src/components/editor/manualEditingAvailability.test.ts +6 -6
  18. package/src/components/editor/manualEditingAvailability.ts +5 -3
  19. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +10 -0
  20. package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -0
  21. package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
  22. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +21 -9
  23. package/src/components/editor/propertyPanelFlatStyleSections.tsx +15 -32
  24. package/src/components/editor/propertyPanelFlatTextSection.tsx +2 -2
  25. package/src/components/editor/propertyPanelInputCoverage.test.tsx +14 -0
  26. package/src/components/editor/propertyPanelPrimitives.tsx +9 -1
  27. package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
  28. package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
  29. package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
  30. package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
  31. package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
  32. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
  33. package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
  34. package/src/components/storyboard/storyboardReviewStage.ts +45 -0
  35. package/src/components/storyboard/useFrameComments.ts +22 -6
  36. package/src/contexts/ViewModeContext.tsx +60 -7
  37. package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
  38. package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
  39. package/src/hooks/useSlideshowTabState.test.ts +96 -0
  40. package/src/hooks/useSlideshowTabState.ts +61 -0
  41. package/src/player/lib/playbackTypes.ts +7 -0
  42. package/src/player/lib/runtimeProtocol.test.ts +6 -1
  43. package/src/player/lib/timelineDOM.ts +8 -11
  44. package/src/player/lib/timelineIframeHelpers.ts +153 -107
  45. package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
  46. package/dist/assets/index-DXbu6IPT.css +0 -1
  47. package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +0 -23
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { TimelineElement } from "../store/playerStore";
14
14
  import type { IframeWindow } from "./playbackTypes";
15
+ import { readClipTiming } from "@hyperframes/core/composition-contract";
15
16
  import {
16
17
  getTimelineElementSelector,
17
18
  getTimelineElementSourceFile,
@@ -38,6 +39,9 @@ export function normalizePreviewViewport(doc: Document, win: Window): void {
38
39
  win.scrollTo({ top: 0, left: 0, behavior: "auto" });
39
40
  }
40
41
 
42
+ // Legacy recovery retained until versioned composition manifests complete
43
+ // their compatibility soak across published CDN runtimes.
44
+ // fallow-ignore-next-line complexity
41
45
  export function autoHealMissingCompositionIds(doc: Document): void {
42
46
  const compositionIdRe = /data-composition-id=["']([^"']+)["']/gi;
43
47
  const referencedIds = new Set<string>();
@@ -258,6 +262,141 @@ export function stopScrubPreviewAudio(): void {
258
262
  // Enrich missing compositions from DOM
259
263
  // ---------------------------------------------------------------------------
260
264
 
265
+ function timelineDuration(iframeWin: IframeWindow, compositionId: string): number {
266
+ return (
267
+ (
268
+ iframeWin.__timelines?.[compositionId] as { duration?: () => number } | undefined
269
+ )?.duration?.() ?? 0
270
+ );
271
+ }
272
+
273
+ function createTimedElementLookup(doc: Document): Map<string, Element> {
274
+ const timedById = new Map<string, Element>();
275
+ for (const timed of doc.querySelectorAll("[data-start]")) {
276
+ for (const id of [
277
+ timed.id,
278
+ timed.getAttribute("data-hf-id"),
279
+ timed.getAttribute("data-composition-id"),
280
+ ]) {
281
+ if (id) timedById.set(id, timed);
282
+ }
283
+ }
284
+ return timedById;
285
+ }
286
+
287
+ function createReferenceEndResolver(
288
+ timedById: ReadonlyMap<string, Element>,
289
+ iframeWin: IframeWindow,
290
+ ): (refId: string, visiting: ReadonlySet<string>) => number | null {
291
+ const resolveEnd = (refId: string, visiting: ReadonlySet<string>): number | null => {
292
+ if (visiting.has(refId)) return null;
293
+ const referenced = timedById.get(refId);
294
+ if (!referenced) return null;
295
+ const next = new Set(visiting).add(refId);
296
+ const timing = readClipTiming(referenced, {
297
+ resolveReferenceEnd: (nestedId) => resolveEnd(nestedId, next),
298
+ });
299
+ if (timing.end != null) return timing.end;
300
+ const compositionId = referenced.getAttribute("data-composition-id");
301
+ const duration = compositionId ? timelineDuration(iframeWin, compositionId) : 0;
302
+ return timing.start == null || duration <= 0 ? null : timing.start + duration;
303
+ };
304
+ return resolveEnd;
305
+ }
306
+
307
+ function clampCompositionWindow(
308
+ start: number,
309
+ duration: number,
310
+ rootDuration: number,
311
+ ): { start: number; duration: number } | null {
312
+ if (!Number.isFinite(duration) || duration <= 0) return null;
313
+ const safeStart = Number.isFinite(start) ? start : 0;
314
+ if (!Number.isFinite(rootDuration) || rootDuration <= 0) {
315
+ return { start: safeStart, duration };
316
+ }
317
+ if (safeStart >= rootDuration) return null;
318
+ const clamped = Math.min(duration, Math.max(0, rootDuration - safeStart));
319
+ return clamped > 0 ? { start: safeStart, duration: clamped } : null;
320
+ }
321
+
322
+ function nonEmpty(value: string, fallback: string): string {
323
+ return value || fallback;
324
+ }
325
+
326
+ function optionalNonEmpty(value: string | null): string | undefined {
327
+ return value || undefined;
328
+ }
329
+
330
+ function attachCompositionSource(
331
+ entry: TimelineElement,
332
+ element: HTMLElement,
333
+ compositionSrc: string | null,
334
+ ): TimelineElement {
335
+ if (compositionSrc) return { ...entry, compositionSrc };
336
+ const innerVideo = element.querySelector("video[src]");
337
+ if (!innerVideo) return entry;
338
+ return { ...entry, src: optionalNonEmpty(innerVideo.getAttribute("src")), tag: "video" };
339
+ }
340
+
341
+ function buildMissingCompositionEntry(params: {
342
+ doc: Document;
343
+ iframeWin: IframeWindow;
344
+ element: HTMLElement;
345
+ compositionId: string;
346
+ rootDuration: number;
347
+ fallbackIndex: number;
348
+ resolveEnd: (refId: string, visiting: ReadonlySet<string>) => number | null;
349
+ }): TimelineElement | null {
350
+ const { doc, iframeWin, element, compositionId, rootDuration, fallbackIndex, resolveEnd } =
351
+ params;
352
+ const timing = readClipTiming(element, {
353
+ resolveReferenceEnd: (refId) => resolveEnd(refId, new Set([compositionId])),
354
+ });
355
+ const window = clampCompositionWindow(
356
+ timing.start ?? 0,
357
+ timing.duration ?? timelineDuration(iframeWin, compositionId),
358
+ rootDuration,
359
+ );
360
+ if (!window) return null;
361
+
362
+ const preferredId = nonEmpty(element.id, compositionId);
363
+ const compositionSrc =
364
+ element.getAttribute("data-composition-src") ?? element.getAttribute("data-composition-file");
365
+ const selector = getTimelineElementSelector(element);
366
+ const sourceFile = getTimelineElementSourceFile(element);
367
+ const selectorIndex = getTimelineElementSelectorIndex(doc, element, selector);
368
+ const label = getTimelineElementDisplayLabel({
369
+ id: preferredId,
370
+ label: element.getAttribute("data-timeline-label") ?? element.getAttribute("data-label"),
371
+ tag: element.tagName,
372
+ });
373
+ const identity = buildTimelineElementIdentity({
374
+ preferredId,
375
+ label,
376
+ fallbackIndex,
377
+ domId: optionalNonEmpty(element.id),
378
+ selector,
379
+ selectorIndex,
380
+ sourceFile,
381
+ });
382
+ const entry: TimelineElement = {
383
+ id: identity.id,
384
+ label,
385
+ key: identity.key,
386
+ tag: element.tagName.toLowerCase(),
387
+ start: window.start,
388
+ duration: window.duration,
389
+ track: timing.trackIndex,
390
+ domId: optionalNonEmpty(element.id),
391
+ hfId: optionalNonEmpty(element.getAttribute("data-hf-id")),
392
+ selector,
393
+ selectorIndex,
394
+ sourceFile,
395
+ zIndex: readTimelineElementZIndex(element),
396
+ };
397
+ return attachCompositionSource(entry, element, compositionSrc);
398
+ }
399
+
261
400
  /**
262
401
  * Scan the iframe DOM for composition hosts missing from the current
263
402
  * timeline elements and add them. The CDN runtime often fails to resolve
@@ -279,117 +418,24 @@ export function buildMissingCompositionElements(
279
418
  const hosts = doc.querySelectorAll("[data-composition-id][data-start]");
280
419
  const missing: TimelineElement[] = [];
281
420
 
282
- hosts.forEach((host) => {
421
+ const resolveEnd = createReferenceEndResolver(createTimedElementLookup(doc), iframeWin);
422
+
423
+ for (const host of hosts) {
283
424
  const el = host as HTMLElement;
284
425
  const compId = el.getAttribute("data-composition-id");
285
- if (!compId || compId === rootCompId) return;
286
- if (existingIds.has(el.id) || existingIds.has(compId)) return;
287
-
288
- // Resolve start: numeric or element-reference
289
- const startAttr = el.getAttribute("data-start") ?? "0";
290
- let start = parseFloat(startAttr);
291
- if (isNaN(start)) {
292
- const ref =
293
- doc.getElementById(startAttr) ||
294
- doc.querySelector(`[data-composition-id="${CSS.escape(startAttr)}"]`);
295
- if (ref) {
296
- const refStartAttr = ref.getAttribute("data-start") ?? "0";
297
- let refStart = parseFloat(refStartAttr);
298
- // Recursively resolve one level of reference for the ref's own start
299
- if (isNaN(refStart)) {
300
- const refRef =
301
- doc.getElementById(refStartAttr) ||
302
- doc.querySelector(`[data-composition-id="${CSS.escape(refStartAttr)}"]`);
303
- const rrStart = parseFloat(refRef?.getAttribute("data-start") ?? "0") || 0;
304
- const rrCompId = refRef?.getAttribute("data-composition-id");
305
- const rrDur =
306
- parseFloat(refRef?.getAttribute("data-duration") ?? "") ||
307
- (rrCompId
308
- ? ((
309
- iframeWin.__timelines?.[rrCompId] as { duration?: () => number } | undefined
310
- )?.duration?.() ?? 0)
311
- : 0);
312
- refStart = rrStart + rrDur;
313
- }
314
- const refCompId = ref.getAttribute("data-composition-id");
315
- const refDur =
316
- parseFloat(ref.getAttribute("data-duration") ?? "") ||
317
- (refCompId
318
- ? ((
319
- iframeWin.__timelines?.[refCompId] as { duration?: () => number } | undefined
320
- )?.duration?.() ?? 0)
321
- : 0);
322
- start = refStart + refDur;
323
- } else {
324
- start = 0;
325
- }
326
- }
327
-
328
- // Resolve duration from data-duration or GSAP timeline
329
- let dur = parseFloat(el.getAttribute("data-duration") ?? "");
330
- if (isNaN(dur) || dur <= 0) {
331
- dur =
332
- (
333
- iframeWin.__timelines?.[compId] as { duration?: () => number } | undefined
334
- )?.duration?.() ?? 0;
335
- }
336
- if (!Number.isFinite(dur) || dur <= 0) return;
337
- if (!Number.isFinite(start)) start = 0;
338
- if (Number.isFinite(rootDuration) && rootDuration > 0) {
339
- if (start >= rootDuration) return;
340
- dur = Math.min(dur, Math.max(0, rootDuration - start));
341
- if (dur <= 0) return;
342
- }
343
-
344
- const trackStr = el.getAttribute("data-track-index");
345
- const track = trackStr != null ? parseInt(trackStr, 10) : 0;
346
- // fallow-ignore-next-line code-duplication
347
- const compSrc =
348
- el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
349
- const selector = getTimelineElementSelector(el);
350
- const sourceFile = getTimelineElementSourceFile(el);
351
- const selectorIndex = getTimelineElementSelectorIndex(doc, el, selector);
352
- const label = getTimelineElementDisplayLabel({
353
- id: el.id || compId || null,
354
- label: el.getAttribute("data-timeline-label") ?? el.getAttribute("data-label"),
355
- tag: el.tagName,
356
- });
357
- const identity = buildTimelineElementIdentity({
358
- preferredId: el.id || compId || null,
359
- label,
426
+ if (!compId || compId === rootCompId) continue;
427
+ if (existingIds.has(el.id) || existingIds.has(compId)) continue;
428
+ const entry = buildMissingCompositionEntry({
429
+ doc,
430
+ iframeWin,
431
+ element: el,
432
+ compositionId: compId,
433
+ rootDuration,
360
434
  fallbackIndex: missing.length,
361
- domId: el.id || undefined,
362
- selector,
363
- selectorIndex,
364
- sourceFile,
435
+ resolveEnd,
365
436
  });
366
- const entry: TimelineElement = {
367
- id: identity.id,
368
- label,
369
- key: identity.key,
370
- tag: el.tagName.toLowerCase(),
371
- start,
372
- duration: dur,
373
- track: isNaN(track) ? 0 : track,
374
- domId: el.id || undefined,
375
- hfId: el.getAttribute("data-hf-id") || undefined,
376
- selector,
377
- selectorIndex,
378
- sourceFile,
379
- zIndex: readTimelineElementZIndex(el),
380
- };
381
- if (compSrc) {
382
- entry.compositionSrc = compSrc;
383
- } else {
384
- // Inline composition — expose inner video for thumbnails
385
- const innerVideo = el.querySelector("video[src]");
386
- if (innerVideo) {
387
- entry.src = innerVideo.getAttribute("src") || undefined;
388
- entry.tag = "video";
389
- }
390
- }
391
- missing.push(entry);
392
- });
437
+ if (entry) missing.push(entry);
438
+ }
393
439
 
394
440
  // Patch existing elements that are missing compositionSrc
395
441
  let patched = false;