@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.
- package/dist/assets/hyperframes-player-Xvx2hkrc.js +459 -0
- package/dist/assets/{index-cH6NfVV_.js → index-2mxh_HSy.js} +177 -177
- package/dist/assets/{index-D6etaey-.js → index-BCpoiv9S.js} +1 -1
- package/dist/assets/index-BhWig0mx.css +1 -0
- package/dist/assets/{index-Dh_WhagG.js → index-D-uFclFj.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +1202 -673
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -8
- package/src/components/DesignPanelPromoteProvider.tsx +27 -1
- package/src/components/StudioHeader.tsx +2 -3
- package/src/components/StudioRightPanel.tsx +34 -26
- package/src/components/editor/PromotableControl.tsx +4 -2
- package/src/components/editor/PropertyPanel.tsx +151 -149
- package/src/components/editor/PropertyPanelFlat.tsx +44 -42
- package/src/components/editor/manualEditingAvailability.test.ts +6 -6
- package/src/components/editor/manualEditingAvailability.ts +5 -3
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +10 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +0 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +21 -9
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +15 -32
- package/src/components/editor/propertyPanelFlatTextSection.tsx +2 -2
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +14 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +9 -1
- package/src/components/storyboard/AgentChatMessageButton.test.tsx +47 -0
- package/src/components/storyboard/AgentChatMessageButton.tsx +45 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +189 -66
- package/src/components/storyboard/StoryboardLoaded.tsx +121 -23
- package/src/components/storyboard/StoryboardReviewGuide.tsx +300 -0
- package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +170 -0
- package/src/components/storyboard/storyboardReviewStage.test.ts +40 -0
- package/src/components/storyboard/storyboardReviewStage.ts +45 -0
- package/src/components/storyboard/useFrameComments.ts +22 -6
- package/src/contexts/ViewModeContext.tsx +60 -7
- package/src/hooks/useAddAssetAtPlayhead.test.ts +44 -0
- package/src/hooks/useAddAssetAtPlayhead.ts +21 -0
- package/src/hooks/useSlideshowTabState.test.ts +96 -0
- package/src/hooks/useSlideshowTabState.ts +61 -0
- package/src/player/lib/playbackTypes.ts +7 -0
- package/src/player/lib/runtimeProtocol.test.ts +6 -1
- package/src/player/lib/timelineDOM.ts +8 -11
- package/src/player/lib/timelineIframeHelpers.ts +153 -107
- package/dist/assets/hyperframes-player-3XTTaVNf.js +0 -459
- package/dist/assets/index-DXbu6IPT.css +0 -1
- 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
|
-
|
|
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)
|
|
286
|
-
if (existingIds.has(el.id) || existingIds.has(compId))
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
362
|
-
selector,
|
|
363
|
-
selectorIndex,
|
|
364
|
-
sourceFile,
|
|
435
|
+
resolveEnd,
|
|
365
436
|
});
|
|
366
|
-
|
|
367
|
-
|
|
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;
|