@hyperframes/studio 0.8.110 → 0.8.112
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-c6WR3RsF.js → hyperframes-player-CBpg41JM.js} +1 -1
- package/dist/assets/{index-BJpQzV6h.js → index-BFitquwi.js} +1 -1
- package/dist/assets/{index-Co6W8IVk.js → index-BKy_birQ.js} +194 -194
- package/dist/assets/{index-C0KfbPAo.js → index-CZfrSyU9.js} +1 -1
- package/dist/assets/{index-C7apTvpr.css → index-DbfKVTId.css} +1 -1
- package/dist/{chunk-PQFEBX32.js → chunk-BIUXP3PG.js} +2 -2
- package/dist/{chunk-6LUNLTHR.js → chunk-K47E2ZJA.js} +13 -8
- package/dist/chunk-K47E2ZJA.js.map +1 -0
- package/dist/{chunk-QUXZVHOJ.js → chunk-VWT7STY3.js} +2 -2
- package/dist/{domEditingLayers-JQPQGGT7.js → domEditingLayers-V67MS4WP.js} +3 -3
- package/dist/index.d.ts +81 -46
- package/dist/index.html +2 -2
- package/dist/index.js +7295 -6844
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +10 -8
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +2 -2
- package/src/hooks/timelineEditingHelpers.ts +7 -0
- package/src/hooks/timelineMoveAdapter.test.ts +12 -0
- package/src/hooks/timelineMoveAdapter.ts +9 -4
- package/src/hooks/useTimelineGroupEditing.test.tsx +77 -0
- package/src/hooks/useTimelineGroupEditing.ts +5 -1
- package/src/index.ts +8 -0
- package/src/main.tsx +4 -0
- package/src/player/components/ClipBadges.tsx +5 -5
- package/src/player/components/Timeline.ghostLane.test.tsx +8 -61
- package/src/player/components/Timeline.trackPadding.test.tsx +11 -56
- package/src/player/components/TimelineCanvas.tsx +6 -2
- package/src/player/components/TimelineClip.filmstrip.test.tsx +156 -0
- package/src/player/components/TimelineClip.test.tsx +7 -7
- package/src/player/components/TimelineClip.tsx +23 -7
- package/src/player/components/TimelineClipFades.tsx +41 -43
- package/src/player/components/TimelineGestureOverlay.test.tsx +41 -0
- package/src/player/components/TimelineGestureOverlay.tsx +59 -21
- package/src/player/components/TimelineLanes.tsx +18 -16
- package/src/player/components/timelineCallbacks.ts +6 -4
- package/src/player/components/timelineClipChildren.tsx +7 -13
- package/src/player/components/timelineClipDragCommit.test.ts +51 -0
- package/src/player/components/timelineClipDragCommit.ts +63 -21
- package/src/player/components/timelineClipDragGestureLifecycle.ts +18 -2
- package/src/player/components/timelineClipDragPreview.test.ts +47 -31
- package/src/player/components/timelineClipDragPreview.ts +81 -17
- package/src/player/components/timelineClipDragTypes.ts +3 -3
- package/src/player/components/timelineClipFadeGeometry.test.ts +29 -0
- package/src/player/components/timelineClipFadeGeometry.ts +74 -0
- package/src/player/components/timelineCollision.ts +51 -11
- package/src/player/components/timelineDragLanding.ts +20 -6
- package/src/player/components/timelineLaneProps.ts +3 -8
- package/src/player/components/timelineMotionStyles.test.ts +6 -13
- package/src/player/components/timelineMountFixtures.tsx +65 -0
- package/src/player/components/timelineOptimisticRevision.ts +15 -4
- package/src/player/components/timelineTheme.ts +2 -0
- package/src/player/components/timelineTrackDisplay.ts +1 -1
- package/src/player/components/timelineTrackInsertLayout.test.ts +249 -0
- package/src/player/components/timelineTrackInsertLayout.ts +71 -0
- package/src/player/components/timelineZMirror.ts +5 -11
- package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +163 -0
- package/src/player/components/useTimelineClipDrag.ts +116 -12
- package/src/player/components/useTimelineDisplayLayout.test.tsx +59 -0
- package/src/player/components/useTimelineKeyboardActor.ts +86 -7
- package/src/player/components/useTimelineProviderState.tsx +3 -1
- package/src/player/components/useTimelineRowVirtualization.test.tsx +56 -0
- package/src/player/components/useTimelineRowVirtualization.ts +3 -1
- package/src/player/components/useTimelineTrackDerivations.test.ts +14 -1
- package/src/player/components/useTimelineTrackDerivations.ts +32 -18
- package/src/player/components/useTimelineTrackLayout.ts +43 -14
- package/src/player/hooks/timelinePlayerSync.test.ts +7 -0
- package/src/player/hooks/timelinePlayerSync.ts +1 -0
- package/src/player/lib/automationStoreSync.test.ts +15 -0
- package/src/player/lib/automationStoreSync.ts +13 -5
- package/src/player/lib/storedVolume.ts +7 -0
- package/src/player/lib/thumbnailPageLifecycle.test.ts +123 -0
- package/src/player/lib/thumbnailPageLifecycle.ts +27 -0
- package/src/player/lib/thumbnailScheduler.ts +20 -2
- package/src/player/lib/timelineElementHelpers.ts +2 -8
- package/src/styles/components.css +71 -19
- package/src/styles/theme.css +4 -1
- package/src/utils/studioPendingEdits.test.ts +18 -0
- package/dist/chunk-6LUNLTHR.js.map +0 -1
- /package/dist/{chunk-PQFEBX32.js.map → chunk-BIUXP3PG.js.map} +0 -0
- /package/dist/{chunk-QUXZVHOJ.js.map → chunk-VWT7STY3.js.map} +0 -0
- /package/dist/{domEditingLayers-JQPQGGT7.js.map → domEditingLayers-V67MS4WP.js.map} +0 -0
|
@@ -24,4 +24,11 @@ describe("timelineElementsChanged", () => {
|
|
|
24
24
|
const linked = { ...clip, link: "lk-1" };
|
|
25
25
|
expect(timelineElementsChanged([linked], [clip])).toBe(true);
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
it("sees restored group membership with identical timing and lane", () => {
|
|
29
|
+
const grouped = { ...clip, audioGroup: "G" };
|
|
30
|
+
expect(timelineElementsChanged([clip], [grouped])).toBe(true);
|
|
31
|
+
expect(timelineElementsChanged([grouped], [clip])).toBe(true);
|
|
32
|
+
expect(timelineElementsChanged([grouped], [{ ...grouped }])).toBe(false);
|
|
33
|
+
});
|
|
27
34
|
});
|
|
@@ -40,9 +40,24 @@ describe("syncStoredElementAttribute", () => {
|
|
|
40
40
|
syncStoredElementAttribute(el(), "data-fade-in", null);
|
|
41
41
|
expect(usePlayerStore.getState().elements[0]?.fadeIn).toBeUndefined();
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
// A quick volume save left the clip at its last load's volume, so a card reopened on it read the old level.
|
|
45
|
+
it("records a saved volume, and drops it when the attribute goes", () => {
|
|
46
|
+
usePlayerStore.setState({ elements: [el({ volume: 0.5 })] });
|
|
47
|
+
syncStoredElementAttribute(el(), "data-volume", "1.5");
|
|
48
|
+
expect(usePlayerStore.getState().elements[0]?.volume).toBe(1.5);
|
|
49
|
+
syncStoredElementAttribute(el(), "data-volume", null);
|
|
50
|
+
expect(usePlayerStore.getState().elements[0]?.volume).toBeUndefined();
|
|
51
|
+
});
|
|
43
52
|
});
|
|
44
53
|
|
|
45
54
|
describe("syncStoredAutomationFromPreview", () => {
|
|
55
|
+
it("reads back a volume an undo restored on the preview", () => {
|
|
56
|
+
usePlayerStore.setState({ elements: [el({ volume: 1.5 })] });
|
|
57
|
+
syncStoredAutomationFromPreview(previewWith({ "data-volume": "0.5" }));
|
|
58
|
+
expect(usePlayerStore.getState().elements[0]?.volume).toBe(0.5);
|
|
59
|
+
});
|
|
60
|
+
|
|
46
61
|
it("reads back a fade an undo restored on the preview", () => {
|
|
47
62
|
usePlayerStore.setState({ elements: [el({ fadeOut: 2 })] });
|
|
48
63
|
syncStoredAutomationFromPreview(previewWith({ "data-fade-in": "0.6" }));
|
|
@@ -24,10 +24,15 @@ import {
|
|
|
24
24
|
} from "@hyperframes/core/audio-fade";
|
|
25
25
|
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
26
26
|
import { groupInfoFor } from "./timelineGroupInfo";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
getTimelineElementIdentity,
|
|
29
|
+
previewElementFinder,
|
|
30
|
+
resolveMediaElement,
|
|
31
|
+
} from "./timelineElementHelpers";
|
|
32
|
+
import { elementVolume, parseStoredVolume } from "./storedVolume";
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
|
-
* Re-read every element's automation, FX-chain and
|
|
35
|
+
* Re-read every element's automation, FX-chain, fade and volume attributes from the preview
|
|
31
36
|
* document, for a change that reached the DOM without going through this store.
|
|
32
37
|
*
|
|
33
38
|
* That is undo and redo. A soft restore patches the reverted attributes onto the
|
|
@@ -40,9 +45,9 @@ import { getTimelineElementIdentity, previewElementFinder } from "./timelineElem
|
|
|
40
45
|
* reverted is only known by looking.
|
|
41
46
|
*/
|
|
42
47
|
/**
|
|
43
|
-
* What an element's
|
|
48
|
+
* What an element's seven synced fields SHOULD read, given the preview.
|
|
44
49
|
*
|
|
45
|
-
* Its own
|
|
50
|
+
* Its own five come off its node; the other two are its copy of what its group
|
|
46
51
|
* carries. The timeline derives a group's lanes and chain from these mirrors,
|
|
47
52
|
* never from the group element, and automating a group writes `data-automation`
|
|
48
53
|
* on the group node through the ordinary element path, so both halves are re-read.
|
|
@@ -55,6 +60,7 @@ function syncedFields(doc: Document, element: TimelineElement, node: Element) {
|
|
|
55
60
|
fxChain: node.getAttribute(HF_AUDIO_FX_ATTR) ?? undefined,
|
|
56
61
|
fadeIn: storedFade(fades.fadeIn),
|
|
57
62
|
fadeOut: storedFade(fades.fadeOut),
|
|
63
|
+
volume: elementVolume(node, resolveMediaElement(node) ?? node),
|
|
58
64
|
audioGroupAutomation: group?.automation,
|
|
59
65
|
audioGroupFxChain: group?.fxChain,
|
|
60
66
|
};
|
|
@@ -88,9 +94,11 @@ const STORED_FIELD: Record<string, (value: string | null) => Partial<TimelineEle
|
|
|
88
94
|
[HF_AUDIO_FX_ATTR]: (value) => ({ fxChain: value ?? undefined }),
|
|
89
95
|
[HF_AUDIO_FADE_IN_ATTR]: (value) => ({ fadeIn: storedFade(readFadeSeconds(value)) }),
|
|
90
96
|
[HF_AUDIO_FADE_OUT_ATTR]: (value) => ({ fadeOut: storedFade(readFadeSeconds(value)) }),
|
|
97
|
+
// A quick volume save writes no other field the store keeps: without this, a clip read its last load's volume.
|
|
98
|
+
"data-volume": (value) => ({ volume: parseStoredVolume(value) }),
|
|
91
99
|
};
|
|
92
100
|
|
|
93
|
-
/** Record a saved automation, FX-chain or
|
|
101
|
+
/** Record a saved automation, FX-chain, fade or volume value on one element's stored copy. */
|
|
94
102
|
export function syncStoredElementAttribute(
|
|
95
103
|
target: TimelineElement,
|
|
96
104
|
attr: string,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function parseStoredVolume(text: string | null): number | undefined {
|
|
2
|
+
const volume = Number.parseFloat(text ?? "");
|
|
3
|
+
return Number.isFinite(volume) ? volume : undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const elementVolume = (el: Element, media: Element) =>
|
|
7
|
+
parseStoredVolume(el.getAttribute("data-volume") ?? media.getAttribute("data-volume"));
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { bindThumbnailPageLifecycle } from "./thumbnailPageLifecycle";
|
|
3
|
+
import {
|
|
4
|
+
ThumbnailScheduler,
|
|
5
|
+
type ThumbnailRequest,
|
|
6
|
+
type ThumbnailLoadedResult,
|
|
7
|
+
} from "./thumbnailScheduler";
|
|
8
|
+
|
|
9
|
+
function mountLifetime() {
|
|
10
|
+
const scheduler = new ThumbnailScheduler();
|
|
11
|
+
const page = new EventTarget();
|
|
12
|
+
const document = Object.assign(new EventTarget(), { hidden: false });
|
|
13
|
+
const unbind = bindThumbnailPageLifecycle(page, document, scheduler);
|
|
14
|
+
return { scheduler, page, document, unbind };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("thumbnail document lifetime", () => {
|
|
18
|
+
it("cancels every active thumbnail kind on pagehide and resumes retained leases", async () => {
|
|
19
|
+
const { scheduler, page, unbind } = mountLifetime();
|
|
20
|
+
const signals: AbortSignal[] = [];
|
|
21
|
+
const kinds = ["composition", "image", "video", "waveform"] as const;
|
|
22
|
+
const requests = kinds.map((kind): ThumbnailRequest => {
|
|
23
|
+
let attempts = 0;
|
|
24
|
+
return {
|
|
25
|
+
key: kind,
|
|
26
|
+
projectId: "scene",
|
|
27
|
+
sessionEpoch: 0,
|
|
28
|
+
kind,
|
|
29
|
+
priority: "visible",
|
|
30
|
+
load: (signal: AbortSignal) => {
|
|
31
|
+
signals.push(signal);
|
|
32
|
+
attempts++;
|
|
33
|
+
if (attempts > 1)
|
|
34
|
+
return Promise.resolve<ThumbnailLoadedResult>({
|
|
35
|
+
value: { kind: "image", url: kind, aspect: 1 },
|
|
36
|
+
weight: 1,
|
|
37
|
+
});
|
|
38
|
+
return new Promise<ThumbnailLoadedResult>((_, reject) =>
|
|
39
|
+
signal.addEventListener("abort", () =>
|
|
40
|
+
reject(new DOMException("Aborted", "AbortError")),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const leases = requests.map((request) => scheduler.acquire(request, () => {}));
|
|
47
|
+
expect(signals).toHaveLength(4);
|
|
48
|
+
page.dispatchEvent(new Event("pagehide"));
|
|
49
|
+
expect(signals.map((signal) => signal.aborted)).toEqual([true, true, true, true]);
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
51
|
+
expect(requests.map((request) => scheduler.getSnapshot(request).status)).toEqual([
|
|
52
|
+
"queued",
|
|
53
|
+
"queued",
|
|
54
|
+
"queued",
|
|
55
|
+
"queued",
|
|
56
|
+
]);
|
|
57
|
+
page.dispatchEvent(new Event("pageshow"));
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
59
|
+
expect(signals).toHaveLength(8);
|
|
60
|
+
expect(requests.map((request) => scheduler.getSnapshot(request).status)).toEqual([
|
|
61
|
+
"ready",
|
|
62
|
+
"ready",
|
|
63
|
+
"ready",
|
|
64
|
+
"ready",
|
|
65
|
+
]);
|
|
66
|
+
leases.forEach((lease) => lease.release());
|
|
67
|
+
unbind();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("holds new jobs while away and preserves a preview reload hold on pageshow", async () => {
|
|
71
|
+
const { scheduler, page, document, unbind } = mountLifetime();
|
|
72
|
+
const started: string[] = [];
|
|
73
|
+
const make = (kind: "composition" | "image"): ThumbnailRequest => ({
|
|
74
|
+
key: kind,
|
|
75
|
+
projectId: "scene",
|
|
76
|
+
kind,
|
|
77
|
+
priority: "visible",
|
|
78
|
+
load: async () => {
|
|
79
|
+
started.push(kind);
|
|
80
|
+
return { value: { kind: "image", url: kind, aspect: 1 }, weight: 1 };
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
scheduler.setPreviewReloading(true);
|
|
84
|
+
page.dispatchEvent(new Event("pagehide"));
|
|
85
|
+
const leases = [make("composition"), make("image")].map((request) =>
|
|
86
|
+
scheduler.acquire(request, () => {}),
|
|
87
|
+
);
|
|
88
|
+
expect(started).toEqual([]);
|
|
89
|
+
document.hidden = true;
|
|
90
|
+
page.dispatchEvent(new Event("pageshow"));
|
|
91
|
+
expect(started).toEqual([]);
|
|
92
|
+
document.hidden = false;
|
|
93
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
94
|
+
expect(started).toEqual(["image"]);
|
|
95
|
+
scheduler.setPreviewReloading(false);
|
|
96
|
+
expect(started).toEqual(["image", "composition"]);
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
98
|
+
leases.forEach((lease) => lease.release());
|
|
99
|
+
unbind();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("removes the lifecycle listeners when unbound", () => {
|
|
103
|
+
const { scheduler, page, unbind } = mountLifetime();
|
|
104
|
+
unbind();
|
|
105
|
+
page.dispatchEvent(new Event("pagehide"));
|
|
106
|
+
let started = false;
|
|
107
|
+
const lease = scheduler.acquire(
|
|
108
|
+
{
|
|
109
|
+
key: "poster",
|
|
110
|
+
projectId: "scene",
|
|
111
|
+
kind: "image",
|
|
112
|
+
priority: "visible",
|
|
113
|
+
load: async () => {
|
|
114
|
+
started = true;
|
|
115
|
+
return { value: { kind: "image", url: "poster", aspect: 1 }, weight: 1 };
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
() => {},
|
|
119
|
+
);
|
|
120
|
+
expect(started).toBe(true);
|
|
121
|
+
lease.release();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { thumbnailScheduler, type ThumbnailScheduler } from "./thumbnailScheduler";
|
|
2
|
+
|
|
3
|
+
export function bindThumbnailPageLifecycle(
|
|
4
|
+
page: EventTarget,
|
|
5
|
+
document: EventTarget & { readonly hidden: boolean },
|
|
6
|
+
scheduler: ThumbnailScheduler = thumbnailScheduler,
|
|
7
|
+
): () => void {
|
|
8
|
+
let left = false;
|
|
9
|
+
const sync = () => scheduler.setPageHidden(left || document.hidden);
|
|
10
|
+
const hide = () => {
|
|
11
|
+
left = true;
|
|
12
|
+
sync();
|
|
13
|
+
};
|
|
14
|
+
const show = () => {
|
|
15
|
+
left = false;
|
|
16
|
+
sync();
|
|
17
|
+
};
|
|
18
|
+
page.addEventListener("pagehide", hide);
|
|
19
|
+
page.addEventListener("pageshow", show);
|
|
20
|
+
document.addEventListener("visibilitychange", sync);
|
|
21
|
+
sync();
|
|
22
|
+
return () => {
|
|
23
|
+
page.removeEventListener("pagehide", hide);
|
|
24
|
+
page.removeEventListener("pageshow", show);
|
|
25
|
+
document.removeEventListener("visibilitychange", sync);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -117,6 +117,7 @@ export class ThumbnailScheduler {
|
|
|
117
117
|
private nextSequence = 1;
|
|
118
118
|
private scrolling = false;
|
|
119
119
|
private previewReloading = false;
|
|
120
|
+
private pageHidden = false;
|
|
120
121
|
private cacheBytes = 0;
|
|
121
122
|
private waveformCacheBytes = 0;
|
|
122
123
|
private readonly activeByBucket = { video: 0, composition: 0, general: 0 };
|
|
@@ -226,11 +227,27 @@ export class ThumbnailScheduler {
|
|
|
226
227
|
}
|
|
227
228
|
for (const entry of this.entries.values()) {
|
|
228
229
|
if (entry.state !== "loading" || entry.request.kind !== "composition") continue;
|
|
229
|
-
entry
|
|
230
|
-
|
|
230
|
+
this.preempt(entry);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
setPageHidden(hidden: boolean): void {
|
|
235
|
+
if (this.pageHidden === hidden) return;
|
|
236
|
+
this.pageHidden = hidden;
|
|
237
|
+
if (!hidden) {
|
|
238
|
+
this.pump();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const entry of this.entries.values()) {
|
|
242
|
+
if (entry.state === "loading") this.preempt(entry);
|
|
231
243
|
}
|
|
232
244
|
}
|
|
233
245
|
|
|
246
|
+
private preempt(entry: ThumbnailEntry): void {
|
|
247
|
+
entry.preempted = true;
|
|
248
|
+
entry.controller?.abort();
|
|
249
|
+
}
|
|
250
|
+
|
|
234
251
|
/** Evict project cache entries that are no longer owned by mounted consumers. */
|
|
235
252
|
invalidateProject(projectId: string): void {
|
|
236
253
|
for (const [key, entry] of this.entries) {
|
|
@@ -269,6 +286,7 @@ export class ThumbnailScheduler {
|
|
|
269
286
|
}
|
|
270
287
|
|
|
271
288
|
private pump(): void {
|
|
289
|
+
if (this.pageHidden) return;
|
|
272
290
|
const queued = Array.from(this.entries.values())
|
|
273
291
|
.filter((entry) => entry.state === "queued" && entry.leases.size > 0)
|
|
274
292
|
.sort((left, right) => {
|
|
@@ -13,6 +13,7 @@ import { isFinitePositive } from "./playbackAdapter";
|
|
|
13
13
|
import { getSourceScopedSelectorIndex } from "../../utils/sourceScopedSelectorIndex";
|
|
14
14
|
import { HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
|
|
15
15
|
import { readElementFades } from "@hyperframes/core/audio-fade";
|
|
16
|
+
import { elementVolume } from "./storedVolume";
|
|
16
17
|
import {
|
|
17
18
|
type AttrReader,
|
|
18
19
|
clampPlaybackRate,
|
|
@@ -213,13 +214,6 @@ function setOptional<K extends keyof TimelineElement>(
|
|
|
213
214
|
else entry[key] = value;
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
function readVolume(el: Element, media: Element): number | undefined {
|
|
217
|
-
const volume = Number.parseFloat(
|
|
218
|
-
el.getAttribute("data-volume") ?? media.getAttribute("data-volume") ?? "",
|
|
219
|
-
);
|
|
220
|
-
return Number.isFinite(volume) ? volume : undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
217
|
/** The compiler's rule (timingCompiler): explicit data-has-audio wins; otherwise an unmuted <video> is audible. */
|
|
224
218
|
export function isVideoAudible(opts: {
|
|
225
219
|
tag: string;
|
|
@@ -251,7 +245,7 @@ function applyAudioMetadataFromElement(entry: TimelineElement, el: Element): voi
|
|
|
251
245
|
});
|
|
252
246
|
setOptional(entry, "hasAudio", sound ? true : undefined);
|
|
253
247
|
setOptional(entry, "muted", muted ? true : undefined);
|
|
254
|
-
setOptional(entry, "volume",
|
|
248
|
+
setOptional(entry, "volume", elementVolume(el, media));
|
|
255
249
|
}
|
|
256
250
|
|
|
257
251
|
function applyFadeMetadataFromElement(entry: TimelineElement, el: Element): void {
|
|
@@ -160,6 +160,35 @@
|
|
|
160
160
|
var(--timeline-clip-shadow);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
.timeline-clip:not(.is-audio) {
|
|
164
|
+
border-width: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.timeline-clip__content {
|
|
168
|
+
border-radius: inherit;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.timeline-clip:not(.is-audio) .timeline-clip__content {
|
|
172
|
+
top: var(--timeline-clip-band-height);
|
|
173
|
+
border-top-left-radius: 0;
|
|
174
|
+
border-top-right-radius: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.timeline-clip::after {
|
|
178
|
+
content: "";
|
|
179
|
+
position: absolute;
|
|
180
|
+
inset: 0;
|
|
181
|
+
z-index: 7;
|
|
182
|
+
border-radius: inherit;
|
|
183
|
+
background: var(--timeline-clip-chip-text);
|
|
184
|
+
opacity: 0;
|
|
185
|
+
pointer-events: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.timeline-clip.is-hovered::after {
|
|
189
|
+
opacity: 0.1;
|
|
190
|
+
}
|
|
191
|
+
|
|
163
192
|
.timeline-transition {
|
|
164
193
|
position: absolute;
|
|
165
194
|
width: 0;
|
|
@@ -304,24 +333,10 @@
|
|
|
304
333
|
border-color: var(--clip-border-hover);
|
|
305
334
|
}
|
|
306
335
|
|
|
307
|
-
.timeline-clip.is-selected
|
|
308
|
-
outline: 1.5px solid var(--timeline-clip-selection);
|
|
309
|
-
outline-offset: -1.5px;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
.timeline-clip.is-selected::after {
|
|
313
|
-
content: "";
|
|
314
|
-
position: absolute;
|
|
315
|
-
inset: 0;
|
|
316
|
-
z-index: 4;
|
|
317
|
-
border-radius: inherit;
|
|
318
|
-
box-shadow: inset 0 0 0 1px var(--timeline-clip-selection-highlight);
|
|
319
|
-
pointer-events: none;
|
|
320
|
-
}
|
|
321
|
-
|
|
336
|
+
.timeline-clip.is-selected,
|
|
322
337
|
.timeline-clip[data-active].is-selected {
|
|
323
|
-
outline:
|
|
324
|
-
outline-offset:
|
|
338
|
+
outline: 1px solid var(--timeline-clip-selection);
|
|
339
|
+
outline-offset: 0;
|
|
325
340
|
}
|
|
326
341
|
|
|
327
342
|
.timeline-clip.is-dragging {
|
|
@@ -351,6 +366,38 @@
|
|
|
351
366
|
white-space: nowrap;
|
|
352
367
|
}
|
|
353
368
|
|
|
369
|
+
.timeline-clip:not(.is-audio) .timeline-clip__label {
|
|
370
|
+
display: flex;
|
|
371
|
+
z-index: auto;
|
|
372
|
+
top: 0;
|
|
373
|
+
left: 0;
|
|
374
|
+
width: 100%;
|
|
375
|
+
max-width: 100%;
|
|
376
|
+
height: var(--timeline-clip-band-height);
|
|
377
|
+
overflow: visible;
|
|
378
|
+
padding: 0 min(6px, 25%);
|
|
379
|
+
border-radius: inherit;
|
|
380
|
+
border-bottom-left-radius: 0;
|
|
381
|
+
border-bottom-right-radius: 0;
|
|
382
|
+
background: var(--timeline-clip-band-bg);
|
|
383
|
+
font-weight: 600;
|
|
384
|
+
line-height: var(--timeline-clip-band-height);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.timeline-clip:not(.is-audio) .timeline-clip__name {
|
|
388
|
+
flex: 1;
|
|
389
|
+
min-width: 0;
|
|
390
|
+
overflow: hidden;
|
|
391
|
+
text-overflow: ellipsis;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.timeline-clip:not(.is-audio) .timeline-clip__label [data-testid="clip-badges"] {
|
|
395
|
+
position: static;
|
|
396
|
+
flex-shrink: 0;
|
|
397
|
+
max-width: 100%;
|
|
398
|
+
margin-top: 2px;
|
|
399
|
+
}
|
|
400
|
+
|
|
354
401
|
/* Link groups share a label colour on the border and the name chip. */
|
|
355
402
|
.timeline-clip[data-link-color] {
|
|
356
403
|
border-color: var(--clip-link-color);
|
|
@@ -358,7 +405,7 @@
|
|
|
358
405
|
|
|
359
406
|
.timeline-clip[data-link-color] .timeline-clip__label {
|
|
360
407
|
box-shadow: inset 3px 0 0 var(--clip-link-color);
|
|
361
|
-
padding-left: 8px;
|
|
408
|
+
padding-left: min(8px, 25%);
|
|
362
409
|
}
|
|
363
410
|
|
|
364
411
|
.timeline-clip.is-audio .timeline-clip__label {
|
|
@@ -371,6 +418,7 @@
|
|
|
371
418
|
position: absolute;
|
|
372
419
|
bottom: 6px;
|
|
373
420
|
left: 12px;
|
|
421
|
+
right: 16px;
|
|
374
422
|
overflow: hidden;
|
|
375
423
|
color: var(--timeline-tick-text);
|
|
376
424
|
font-family: var(--font-mono);
|
|
@@ -383,7 +431,11 @@
|
|
|
383
431
|
|
|
384
432
|
@media (prefers-reduced-motion: no-preference) {
|
|
385
433
|
.timeline-clip {
|
|
386
|
-
transition: background-color
|
|
434
|
+
transition: background-color var(--timeline-clip-hover-duration) ease-out;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.timeline-clip::after {
|
|
438
|
+
transition: opacity var(--timeline-clip-hover-duration) ease-out;
|
|
387
439
|
}
|
|
388
440
|
|
|
389
441
|
.timeline-clip__handle-bar {
|
package/src/styles/theme.css
CHANGED
|
@@ -119,7 +119,10 @@
|
|
|
119
119
|
--timeline-clip-audio-muted-bg: rgb(72, 74, 78);
|
|
120
120
|
--timeline-clip-selection: light-dark(#0f766e, rgba(255, 255, 255, 0.85));
|
|
121
121
|
--timeline-clip-selection-highlight: rgb(255 255 255 / 0.55);
|
|
122
|
-
--timeline-clip-radius:
|
|
122
|
+
--timeline-clip-radius: 12px;
|
|
123
|
+
--timeline-clip-band-height: 15px;
|
|
124
|
+
--timeline-clip-band-bg: rgb(12 12 14);
|
|
125
|
+
--timeline-clip-hover-duration: 60ms;
|
|
123
126
|
--timeline-clip-audio-radius: 21px;
|
|
124
127
|
--timeline-clip-highlight: rgb(255 255 255 / 0.08);
|
|
125
128
|
--timeline-clip-shadow: 0 2px 6px rgb(0 0 0 / 0.3);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
trackedStudioEdit,
|
|
11
11
|
} from "./studioPendingEdits";
|
|
12
12
|
import { StudioFileConflictError } from "./studioSaveDiagnostics";
|
|
13
|
+
import { revertNewestStudioPendingEdit as hostRevert } from "../index";
|
|
13
14
|
|
|
14
15
|
describe("studio pending edit flush", () => {
|
|
15
16
|
it("waits for mounted panels to persist pending local edits", async () => {
|
|
@@ -277,3 +278,20 @@ describe("a pending edit whose start throws", () => {
|
|
|
277
278
|
expect(hasStudioPendingEdits()).toBe(false);
|
|
278
279
|
});
|
|
279
280
|
});
|
|
281
|
+
|
|
282
|
+
describe("the package's public revert", () => {
|
|
283
|
+
it("lets a host's own undo key paint a still-saving move back at once", async () => {
|
|
284
|
+
let left = "120px";
|
|
285
|
+
const edit = beginStudioPendingEdit(() => ((left = "0px"), () => void (left = "120px")));
|
|
286
|
+
try {
|
|
287
|
+
const putBack = hostRevert();
|
|
288
|
+
expect(left).toBe("0px");
|
|
289
|
+
expect(hostRevert()).toBeNull();
|
|
290
|
+
putBack?.();
|
|
291
|
+
expect(left).toBe("120px");
|
|
292
|
+
} finally {
|
|
293
|
+
edit.settle();
|
|
294
|
+
await flushStudioPendingEdits();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/sourceScopedSelectorIndex.ts","../src/player/lib/playbackAdapter.ts","../src/player/lib/timelineElementHelpers.ts","../src/utils/projectRouting.ts"],"sourcesContent":["/**\n * Occurrence index for a selector within its source document.\n *\n * The preview flattens multiple composition files into one DOM, so a raw\n * `querySelectorAll` index is not a stable source-file identity. Callers supply\n * their existing source resolver; this helper alone owns occurrence scoping.\n */\n\n/** Every element matching one selector, mapped to the source file it belongs to\n * and its occurrence index within that file. One document query fills it. */\ntype SelectorOccurrences = Map<Element, { scope: string; index: number }>;\n\ninterface SelectorIndexPass {\n doc: Document;\n bySelector: Map<string, SelectorOccurrences>;\n}\n\nlet activePass: SelectorIndexPass | null = null;\n\n/**\n * Share one occurrence index per selector across every\n * `getSourceScopedSelectorIndex` call made synchronously inside `run`.\n *\n * Unshared, each call runs its own whole-document `querySelectorAll(selector)`\n * and resolves the source file of every match. A rebuild that walks n elements\n * sharing a selector therefore does n whole-document queries and n² resolver\n * calls — a composition of repeated cards or tiles is exactly that shape. One\n * index per selector makes the same pass linear.\n *\n * Correctness rests on the pass being ONE synchronous walk of a document that\n * does not change under it: nothing invalidates the index, so `run` must not\n * mutate `doc`, and every call inside it must resolve source files the same way\n * (the resolver may be a fresh closure per call, as long as it answers the\n * same). Outside a pass the index is per-call, exactly as before.\n */\nexport function withSelectorIndexPass<T>(doc: Document, run: () => T): T {\n const previous = activePass;\n activePass = { doc, bySelector: new Map() };\n try {\n return run();\n } finally {\n activePass = previous;\n }\n}\n\nfunction buildOccurrences(\n doc: Document,\n selector: string,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): SelectorOccurrences {\n const occurrences: SelectorOccurrences = new Map();\n const counts = new Map<string, number>();\n for (const candidate of doc.querySelectorAll(selector)) {\n const scope = resolveSourceFile(candidate) ?? \"index.html\";\n const index = counts.get(scope) ?? 0;\n counts.set(scope, index + 1);\n occurrences.set(candidate, { scope, index });\n }\n return occurrences;\n}\n\nexport function getSourceScopedSelectorIndex(\n doc: Document,\n el: Element,\n selector: string | undefined,\n sourceFile: string | undefined,\n resolveSourceFile: (candidate: Element) => string | undefined,\n): number | undefined {\n if (!selector || selector.startsWith(\"#\") || selector.startsWith(\"[data-composition-id=\")) {\n return undefined;\n }\n\n try {\n const pass = activePass?.doc === doc ? activePass : null;\n let occurrences = pass?.bySelector.get(selector);\n if (!occurrences) {\n occurrences = buildOccurrences(doc, selector, resolveSourceFile);\n pass?.bySelector.set(selector, occurrences);\n }\n // An element outside the requested scope is not in that scope's occurrence\n // run at all, which is the `indexOf` miss the filtered form returned before.\n const hit = occurrences.get(el);\n return hit && hit.scope === (sourceFile ?? \"index.html\") ? hit.index : undefined;\n } catch {\n return undefined;\n }\n}\n","/**\n * Playback adapter utilities: factory for the static-seek adapter used when a\n * composition exposes only a `renderSeek` / `seek` API (no native play/pause\n * support), plus a thin wrapper that normalises GSAP-style `TimelineLike`\n * objects to the `PlaybackAdapter` interface.\n */\n\nimport type {\n PlaybackAdapter,\n RuntimePlaybackAdapter,\n StaticSeekPlaybackClock,\n TimelineLike,\n} from \"./playbackTypes\";\n\n// ---------------------------------------------------------------------------\n// Pure numeric helpers\n// ---------------------------------------------------------------------------\n\nexport function isFinitePositive(value: number): boolean {\n return Number.isFinite(value) && value > 0;\n}\n\nfunction clampTime(time: number, duration: number): number {\n const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);\n const safeTime = Math.max(0, Number.isFinite(time) ? time : 0);\n return safeDuration > 0 ? Math.min(safeTime, safeDuration) : safeTime;\n}\n\nexport function getAdapterDuration(adapter: PlaybackAdapter | null | undefined): number {\n if (!adapter) return 0;\n try {\n const duration = Number(adapter.getDuration());\n return isFinitePositive(duration) ? duration : 0;\n } catch {\n return 0;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Clock factory\n// ---------------------------------------------------------------------------\n\nexport function getDefaultStaticSeekPlaybackClock(win: Window): StaticSeekPlaybackClock {\n return {\n now: () => win.performance.now(),\n requestAnimationFrame: (callback) => win.requestAnimationFrame(callback),\n cancelAnimationFrame: (handle) => win.cancelAnimationFrame(handle),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Static-seek adapter\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a render-only player (exposes `renderSeek`/`seek` but no native\n * play/pause) and drives playback via `requestAnimationFrame`.\n */\nexport function createStaticSeekPlaybackAdapter(\n player: Pick<RuntimePlaybackAdapter, \"getTime\"> &\n Partial<Pick<RuntimePlaybackAdapter, \"renderSeek\" | \"seek\">>,\n duration: number,\n clock: StaticSeekPlaybackClock,\n getPlaybackRate: () => number = () => 1,\n): PlaybackAdapter {\n const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);\n let currentTime = clampTime(Number(player.getTime?.() ?? 0), safeDuration);\n let playing = false;\n let rafId = 0;\n let playStartTime = currentTime;\n let playStartNow = clock.now();\n\n const renderSeek = (time: number) => {\n currentTime = clampTime(time, safeDuration);\n if (typeof player.renderSeek === \"function\") {\n player.renderSeek(currentTime);\n return;\n }\n player.seek?.(currentTime);\n };\n\n const stopTicker = () => {\n if (rafId) {\n clock.cancelAnimationFrame(rafId);\n rafId = 0;\n }\n };\n\n const tick: FrameRequestCallback = (now) => {\n if (!playing) return;\n const playbackRate = Math.max(0.1, Number(getPlaybackRate()) || 1);\n const elapsed = ((now - playStartNow) / 1000) * playbackRate;\n renderSeek(playStartTime + elapsed);\n if (currentTime >= safeDuration) {\n playing = false;\n rafId = 0;\n return;\n }\n rafId = clock.requestAnimationFrame(tick);\n };\n\n return {\n play: () => {\n if (playing || safeDuration <= 0) return;\n if (currentTime >= safeDuration) renderSeek(0);\n playing = true;\n playStartTime = currentTime;\n playStartNow = clock.now();\n stopTicker();\n rafId = clock.requestAnimationFrame(tick);\n },\n pause: () => {\n playing = false;\n stopTicker();\n },\n seek: (time, options) => {\n renderSeek(time);\n if (options?.keepPlaying) {\n if (playing) {\n playStartTime = currentTime;\n playStartNow = clock.now();\n }\n return;\n }\n // Default seek aligns with wrapTimeline: stop the RAF ticker so the\n // adapter's `playing` flag matches the public seek contract instead of\n // silently driving renderSeek in the background.\n playing = false;\n stopTicker();\n },\n getTime: () => currentTime,\n getDuration: () => safeDuration,\n isPlaying: () => playing,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Static-seek fallback cache\n// ---------------------------------------------------------------------------\n\nexport type StaticSeekCacheEntry = {\n player: RuntimePlaybackAdapter | PlaybackAdapter;\n duration: number;\n adapter: PlaybackAdapter;\n};\n\ntype StaticSeekCacheRef = { current: StaticSeekCacheEntry | null };\ntype WarnedRef = { current: boolean };\n\n/**\n * Pause and drop the cached static-seek adapter. Must be called whenever\n * adapter selection switches to a native adapter — a cached static-seek\n * adapter that was mid-play keeps its private rAF loop seeking the player\n * forever otherwise, fighting the native transport. Also re-arms the\n * downgrade warning so a later re-downgrade is surfaced again.\n */\nexport function releaseStaticSeekCache(cache: StaticSeekCacheRef, warned: WarnedRef): void {\n cache.current?.adapter.pause();\n cache.current = null;\n warned.current = false;\n}\n\n/**\n * Resolve (with caching) the seek-driven fallback adapter. Warns once per\n * downgrade streak: seek-driven playback never starts media elements or\n * WebAudio, so without the warning the downgrade silently loses audio.\n */\nexport function resolveStaticSeekFallback(opts: {\n cache: StaticSeekCacheRef;\n warned: WarnedRef;\n bestAdapter: RuntimePlaybackAdapter | PlaybackAdapter;\n effectiveDuration: number;\n docDuration: number;\n clock: StaticSeekPlaybackClock;\n getPlaybackRate: () => number;\n}): PlaybackAdapter {\n const { cache, warned, bestAdapter, effectiveDuration, docDuration } = opts;\n const cached = cache.current;\n if (cached?.player === bestAdapter && cached.duration === effectiveDuration) {\n return cached.adapter;\n }\n cached?.adapter.pause();\n if (!warned.current) {\n warned.current = true;\n console.warn(\n `[useTimelinePlayer] Selected adapter duration (${getAdapterDuration(bestAdapter)}s) does not cover the document duration (${docDuration}s); falling back to seek-driven playback, which never starts media elements or WebAudio. Audio will not play in preview — extend the GSAP timeline to cover the declared data-duration.`,\n );\n }\n const adapter = createStaticSeekPlaybackAdapter(\n bestAdapter,\n effectiveDuration,\n opts.clock,\n opts.getPlaybackRate,\n );\n cache.current = { player: bestAdapter, duration: effectiveDuration, adapter };\n return adapter;\n}\n\n// ---------------------------------------------------------------------------\n// GSAP timeline wrapper\n// ---------------------------------------------------------------------------\n\nexport function wrapTimeline(tl: TimelineLike): PlaybackAdapter {\n return {\n play: () => tl.play(),\n pause: () => tl.pause(),\n seek: (t, options) => {\n const shouldPause = !options?.keepPlaying;\n if (shouldPause) tl.pause();\n tl.seek(t);\n if (shouldPause) tl.pause();\n },\n getTime: () => tl.time(),\n getDuration: () => tl.duration(),\n isPlaying: () => tl.isActive(),\n };\n}\n","/**\n * Low-level helpers for building and identifying TimelineElement objects.\n *\n * Covers: duration reading, media-element metadata extraction, selector/key/\n * identity builders, and DOM node lookup. These are\n * intentionally dependency-free (no store, no hooks) so they can be used in\n * both the React hook and test environments.\n */\n\nimport type { TimelineElement } from \"../store/playerStore\";\nimport type { ClipManifestClip } from \"./playbackTypes\";\nimport { isFinitePositive } from \"./playbackAdapter\";\nimport { getSourceScopedSelectorIndex } from \"../../utils/sourceScopedSelectorIndex\";\nimport { HF_AUDIO_GROUP_TAG } from \"@hyperframes/core/audio-groups\";\nimport { readElementFades } from \"@hyperframes/core/audio-fade\";\nimport {\n type AttrReader,\n clampPlaybackRate,\n readMediaOffsetSeconds,\n} from \"@hyperframes/parsers/media-duration\";\n\n// ---------------------------------------------------------------------------\n// Layer-reveal lift transparency\n// ---------------------------------------------------------------------------\n\n/**\n * Attributes carrying the pre-lift state of a Layers-panel selection reveal\n * (useLayerRevealOverride): while a layer is selected it PAINTS on top via a\n * temporary inline z-index, but the lift is a purely visual, ephemeral studio\n * affordance — every z reader must keep reporting the element's TRUE z\n * (stored here) so menus, badges, the lane mirror, and the panel sort never\n * reason on the lifted value. A z-reorder commit removes the attributes (the\n * commit is the new truth).\n */\nexport const LAYER_REVEAL_PRIOR_Z_ATTR = \"data-hf-reveal-prior-z\";\nexport const LAYER_REVEAL_PRIOR_POSITION_ATTR = \"data-hf-reveal-prior-pos\";\n\n/** The lifted element's true (pre-lift) z, or null when no lift is active. */\nexport function readLayerRevealPriorZ(el: Element): number | null {\n const raw = el.getAttribute(LAYER_REVEAL_PRIOR_Z_ATTR);\n if (raw == null) return null;\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) ? n : null;\n}\n\n// ---------------------------------------------------------------------------\n// Duration attribute helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read a host element's effective CSS stacking order for the timeline's reverse\n * z→lane mapping. Prefers the inline `style.zIndex` (what the canvas context\n * menu and LayersPanel z-edits write via handleDomZIndexReorderCommit), falls\n * back to computed style; \"auto\" / empty / unparseable ⇒ 0. Works with a\n * detached parse Document (no defaultView) as well as a live iframe. Mirrors\n * canvasContextMenuZOrder.parseZIndex semantics so the two directions agree.\n * Reveal-lift transparent: an active lift reports the stored TRUE z.\n */\nexport function readTimelineElementZIndex(el: Element): number {\n const prior = readLayerRevealPriorZ(el);\n if (prior != null) return prior;\n const html = el as HTMLElement;\n const parseZ = (value: string | null | undefined): number | null => {\n if (value == null || value === \"\" || value === \"auto\") return null;\n const n = Number.parseInt(value, 10);\n return Number.isFinite(n) ? n : null;\n };\n const fromInline = parseZ(html.style?.zIndex);\n if (fromInline != null) return fromInline;\n const view = el.ownerDocument?.defaultView;\n if (view?.getComputedStyle) {\n const fromComputed = parseZ(view.getComputedStyle(html).zIndex);\n if (fromComputed != null) return fromComputed;\n }\n return 0;\n}\n\nfunction readDurationAttribute(el: Element | null | undefined): number {\n if (!el) return 0;\n const duration =\n Number.parseFloat(el.getAttribute(\"data-duration\") ?? \"\") ||\n Number.parseFloat(el.getAttribute(\"data-hf-authored-duration\") ?? \"\");\n return isFinitePositive(duration) ? duration : 0;\n}\n\nexport function isTimelineIgnoredElement(el: Element): boolean {\n // An `<hf-audio-group>` is a mixer bus with no timing of its own, drawn as a group row, never a clip.\n if (el.tagName.toLowerCase() === HF_AUDIO_GROUP_TAG) return true;\n return Boolean(\n el.closest(\n [\n \"[data-hyperframes-ignore]\",\n \"[data-hyperframes-picker-ignore]\",\n \"[data-hf-ignore]\",\n \"[data-hf-color-grading-canvas]\",\n ].join(\",\"),\n ),\n );\n}\n\n/**\n * Furthest clip end (start + RAW `data-duration`) over every non-root clip in the\n * document. Reads the authored attribute, NOT any runtime-computed value — so it\n * is immune to the runtime's clamp that truncates a clip's live duration to the\n * composition length. This is the source of truth for content-driven duration:\n * computing it from the store instead would feed the truncated value back in and\n * make the composition length ratchet down (research HANDOFF-3 §6.1 feedback loop).\n */\nexport function furthestClipEndFromDocument(doc: Document | null | undefined): number {\n if (!doc) return 0;\n const root = doc.querySelector(\"[data-composition-id]\");\n let maxEnd = 0;\n for (const node of Array.from(doc.querySelectorAll(\"[data-start]\"))) {\n if (node === root || isTimelineIgnoredElement(node)) continue;\n const start = Number.parseFloat(node.getAttribute(\"data-start\") ?? \"\");\n const duration = readDurationAttribute(node);\n if (!Number.isFinite(start) || start < 0 || duration <= 0) continue;\n maxEnd = Math.max(maxEnd, start + duration);\n }\n return maxEnd;\n}\n\nexport function readTimelineDurationFromDocument(doc: Document | null | undefined): number {\n if (!doc) return 0;\n const rootDuration = readDurationAttribute(doc.querySelector(\"[data-composition-id]\"));\n if (rootDuration > 0) return rootDuration;\n return furthestClipEndFromDocument(doc);\n}\n\n/**\n * Furthest clip end parsed straight from a composition SOURCE STRING (the HTML\n * being saved). Uses raw `data-duration`, so it is the correct input for syncing\n * the root duration after an edit — reading the store instead would use the\n * runtime-truncated durations and shrink the composition (the feedback loop).\n */\nexport function furthestClipEndFromSource(source: string): number {\n if (!source) return 0;\n return furthestClipEndFromDocument(new DOMParser().parseFromString(source, \"text/html\"));\n}\n\n// ---------------------------------------------------------------------------\n// DOM element type guards\n// ---------------------------------------------------------------------------\n\nfunction isHtmlElement(el: Element): el is HTMLElement {\n const HtmlElementCtor = el.ownerDocument.defaultView?.HTMLElement ?? globalThis.HTMLElement;\n return typeof HtmlElementCtor !== \"undefined\" && el instanceof HtmlElementCtor;\n}\n\nfunction isCompositionHost(el: Element): boolean {\n return (\n el.hasAttribute(\"data-composition-id\") ||\n el.hasAttribute(\"data-composition-src\") ||\n el.hasAttribute(\"data-composition-file\")\n );\n}\n\nexport function resolveMediaElement(el: Element): HTMLMediaElement | HTMLImageElement | null {\n const win = el.ownerDocument.defaultView ?? window;\n const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;\n const ImageElementCtor = win.HTMLImageElement ?? globalThis.HTMLImageElement;\n if (el instanceof MediaElementCtor || el instanceof ImageElementCtor) return el;\n // A composition's media belongs to its own timeline, not the clip's: its length would cap a trim.\n if (isCompositionHost(el)) return null;\n const candidate = el.querySelector(\"video, audio, img\");\n return candidate instanceof MediaElementCtor || candidate instanceof ImageElementCtor\n ? candidate\n : null;\n}\n\n/** The in-point as playback reads it, and the attribute holding it; empty when neither is authored. */\nexport function readPlaybackStartAttributes(\n getAttr: AttrReader,\n): Pick<TimelineElement, \"playbackStart\" | \"playbackStartAttr\"> {\n const playbackStartAttr =\n getAttr(\"data-playback-start\") != null\n ? \"playback-start\"\n : getAttr(\"data-media-start\") != null\n ? \"media-start\"\n : undefined;\n return playbackStartAttr\n ? { playbackStart: readMediaOffsetSeconds(getAttr), playbackStartAttr }\n : {};\n}\n\nexport function playbackStartAttributeForElement(\n element: Pick<TimelineElement, \"kind\" | \"playbackStartAttr\">,\n): \"data-media-start\" | \"data-playback-start\" {\n return element.playbackStartAttr === \"playback-start\" || element.kind === \"composition\"\n ? \"data-playback-start\"\n : \"data-media-start\";\n}\n\nfunction applyPlaybackMetadataFromElement(entry: TimelineElement, el: Element): void {\n Object.assign(\n entry,\n readPlaybackStartAttributes((n) => el.getAttribute(n)),\n );\n\n const authoredPlaybackRate = Number.parseFloat(el.getAttribute(\"data-playback-rate\") ?? \"\");\n if (Number.isFinite(authoredPlaybackRate) && authoredPlaybackRate > 0) {\n entry.playbackRate = clampPlaybackRate(authoredPlaybackRate);\n }\n}\n\n/** Sets or clears an optional field, so a re-parse after the attribute is removed drops it. */\nfunction setOptional<K extends keyof TimelineElement>(\n entry: TimelineElement,\n key: K,\n value: TimelineElement[K] | undefined,\n): void {\n if (value === undefined) delete entry[key];\n else entry[key] = value;\n}\n\nfunction readVolume(el: Element, media: Element): number | undefined {\n const volume = Number.parseFloat(\n el.getAttribute(\"data-volume\") ?? media.getAttribute(\"data-volume\") ?? \"\",\n );\n return Number.isFinite(volume) ? volume : undefined;\n}\n\n/** The compiler's rule (timingCompiler): explicit data-has-audio wins; otherwise an unmuted <video> is audible. */\nexport function isVideoAudible(opts: {\n tag: string;\n hasAudioAttr: string | null | undefined;\n muted: boolean;\n}): boolean {\n if (opts.hasAudioAttr === \"true\") return true;\n if (opts.hasAudioAttr === \"false\" || opts.hasAudioAttr === \"\") return false;\n return opts.tag.toLowerCase() === \"video\" && !opts.muted;\n}\n\nexport function isAudibleVideoNode(el: Element): boolean {\n if (el.tagName.toLowerCase() !== \"video\" || el.hasAttribute(\"muted\")) return false;\n return isVideoAudible({\n tag: \"video\",\n hasAudioAttr: el.getAttribute(\"data-has-audio\"),\n muted: false,\n });\n}\n\n/** What the mixer gets: the compiler's `data-has-audio` rule, muted and volume. */\nfunction applyAudioMetadataFromElement(entry: TimelineElement, el: Element): void {\n const media = resolveMediaElement(el) ?? el;\n const muted = el.hasAttribute(\"muted\") || media.hasAttribute(\"muted\");\n const sound = isVideoAudible({\n tag: el.tagName,\n hasAudioAttr: el.getAttribute(\"data-has-audio\"),\n muted,\n });\n setOptional(entry, \"hasAudio\", sound ? true : undefined);\n setOptional(entry, \"muted\", muted ? true : undefined);\n setOptional(entry, \"volume\", readVolume(el, media));\n}\n\nfunction applyFadeMetadataFromElement(entry: TimelineElement, el: Element): void {\n const fades = readElementFades(el);\n setOptional(entry, \"fadeIn\", fades.fadeIn > 0 ? fades.fadeIn : undefined);\n setOptional(entry, \"fadeOut\", fades.fadeOut > 0 ? fades.fadeOut : undefined);\n}\n\nexport function applyMediaMetadataFromElement(entry: TimelineElement, el: Element): void {\n applyPlaybackMetadataFromElement(entry, el);\n applyAudioMetadataFromElement(entry, el);\n applyFadeMetadataFromElement(entry, el);\n\n const mediaEl = resolveMediaElement(el);\n if (!mediaEl) return;\n\n entry.tag = mediaEl.tagName.toLowerCase();\n const src = mediaEl.getAttribute(\"src\");\n if (src) entry.src = src;\n\n const win = mediaEl.ownerDocument.defaultView ?? window;\n const MediaElementCtor = win.HTMLMediaElement ?? globalThis.HTMLMediaElement;\n if (typeof MediaElementCtor === \"undefined\" || !(mediaEl instanceof MediaElementCtor)) return;\n\n const sourceDurationAttr =\n el.getAttribute(\"data-source-duration\") ?? mediaEl.getAttribute(\"data-source-duration\");\n const sourceDuration = sourceDurationAttr ? parseFloat(sourceDurationAttr) : mediaEl.duration;\n if (Number.isFinite(sourceDuration) && sourceDuration > 0) {\n entry.sourceDuration = sourceDuration;\n }\n\n const playbackRate = mediaEl.defaultPlaybackRate;\n if (entry.playbackRate == null && Number.isFinite(playbackRate) && playbackRate > 0) {\n entry.playbackRate = clampPlaybackRate(playbackRate);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Label helpers\n// ---------------------------------------------------------------------------\n\nexport function getTimelineElementDisplayLabel(input: {\n id?: string | null;\n label?: string | null;\n tag?: string | null;\n}): string {\n const label = input.label?.trim();\n if (label) return label;\n const id = input.id?.trim();\n if (id) return id;\n const tag = input.tag?.trim().toLowerCase();\n return tag ? `${tag} clip` : \"Timeline clip\";\n}\n\n// ---------------------------------------------------------------------------\n// Selector / identity / key builders\n// ---------------------------------------------------------------------------\n\nexport function getTimelineElementSelector(el: Element): string | undefined {\n if (isHtmlElement(el) && el.id) return `#${CSS.escape(el.id)}`;\n const compId = el.getAttribute(\"data-composition-id\");\n if (compId) return `[data-composition-id=\"${CSS.escape(compId)}\"]`;\n if (isHtmlElement(el)) {\n const classes = el.className.split(/\\s+/).filter(Boolean);\n const firstClass = classes.find((className) => className !== \"clip\") ?? classes[0];\n if (firstClass) return `.${CSS.escape(firstClass)}`;\n }\n return undefined;\n}\n\nexport function getTimelineElementSourceFile(el: Element): string | undefined {\n const ownerRoot = el.parentElement?.closest(\"[data-composition-id]\");\n return (\n ownerRoot?.getAttribute(\"data-composition-file\") ??\n ownerRoot?.getAttribute(\"data-composition-src\") ??\n undefined\n );\n}\n\nexport function getTimelineElementSelectorIndex(\n doc: Document,\n el: Element,\n selector: string | undefined,\n): number | undefined {\n return getSourceScopedSelectorIndex(\n doc,\n el,\n selector,\n getTimelineElementSourceFile(el),\n getTimelineElementSourceFile,\n );\n}\n\nexport function buildTimelineElementKey(params: {\n id: string;\n fallbackIndex: number;\n domId?: string;\n selector?: string;\n selectorIndex?: number;\n sourceFile?: string;\n}): string {\n const scope = params.sourceFile ?? \"index.html\";\n if (params.domId) return `${scope}#${params.domId}`;\n if (params.selector) return `${scope}:${params.selector}:${params.selectorIndex ?? 0}`;\n return `${scope}:${params.id}:${params.fallbackIndex}`;\n}\n\n/**\n * Inverse of {@link buildTimelineElementKey} for the `sourceFile#domId` form.\n * A key with no `#` is a bare dom id and carries no source file of its own, so\n * the caller supplies the scope it wants to look that id up in.\n */\nexport function splitTimelineElementKey(key: string): {\n sourceFile: string | null;\n domId: string;\n} {\n const hashIndex = key.lastIndexOf(\"#\");\n if (hashIndex < 0) return { sourceFile: null, domId: key };\n return { sourceFile: key.slice(0, hashIndex), domId: key.slice(hashIndex + 1) };\n}\n\nexport function buildTimelineElementIdentity(params: {\n preferredId?: string | null;\n label: string;\n fallbackIndex: number;\n domId?: string;\n selector?: string;\n selectorIndex?: number;\n sourceFile?: string;\n}): { id: string; key: string } {\n const id =\n params.preferredId?.trim() ||\n buildTimelineElementKey({\n id: params.label,\n fallbackIndex: params.fallbackIndex,\n domId: params.domId,\n selector: params.selector,\n selectorIndex: params.selectorIndex,\n sourceFile: params.sourceFile,\n });\n const key = buildTimelineElementKey({\n id,\n fallbackIndex: params.fallbackIndex,\n domId: params.domId,\n selector: params.selector,\n selectorIndex: params.selectorIndex,\n sourceFile: params.sourceFile,\n });\n return { id, key };\n}\n\nexport function getTimelineElementIdentity(element: { key?: string | null; id: string }): string {\n return element.key ?? element.id;\n}\n\n/**\n * The id space the RUNTIME matches on — a bare DOM id, never a store key.\n *\n * Studio addresses rows by `buildTimelineElementKey`'s composite\n * `<sourceFile>#<domId>`, but everything audio in `@hyperframes/core` keys off\n * the live document: `resolveAudioGroups` collects `member.id`,\n * `resolveCarveSourceIds` goes through `getElementById`. Anything crossing into\n * that space — a group membership list, a carve source — has to be\n * converted here first; a composite key silently matches nothing.\n *\n * `null` for a row with no DOM id at all (selector-addressed elements): such an\n * element cannot be grouped, because `resolveAudioGroups` skips\n * members without an `id` and would build a group that is half there.\n */\nexport function runtimeAudioId(element: { domId?: string | null }): string | null {\n return element.domId || null;\n}\n\n/**\n * Timeline store key for a z-reorder entry built OUTSIDE the timeline\n * expansion (canvas context menu / LayersPanel), so the reorder commit can\n * update the store's zIndex synchronously. Matches buildTimelineElementKey's\n * stable branches (`sourceFile#domId`, else the selector-based key). Undefined\n * when the element has neither a DOM id nor a selector — the timeline's\n * fallback branch needs its own fallbackIndex, which these callers don't have,\n * so such an entry simply skips the synchronous store update.\n */\nexport function deriveTimelineStoreKey(params: {\n domId?: string;\n selector?: string;\n selectorIndex?: number;\n sourceFile?: string;\n}): string | undefined {\n if (!params.domId && !params.selector) return undefined;\n return buildTimelineElementKey({ id: \"\", fallbackIndex: 0, ...params });\n}\n\n/**\n * {@link deriveTimelineStoreKey} for a caller that already has a DOM id in\n * hand (e.g. one it just minted), so the undefined branch never applies.\n */\nexport function deriveTimelineStoreKeyForDomId(domId: string, sourceFile?: string): string {\n return deriveTimelineStoreKey({ domId, sourceFile })!;\n}\n\n// ---------------------------------------------------------------------------\n// DOM node querying\n// ---------------------------------------------------------------------------\n\nfunction getTimelineDomNodes(doc: Document): Element[] {\n const rootComp = doc.querySelector(\"[data-composition-id]\");\n return Array.from(doc.querySelectorAll(\"[data-start]\")).filter(\n (node) => node !== rootComp && !isTimelineIgnoredElement(node),\n );\n}\n\nfunction numbersNearlyEqual(a: number, b: number): boolean {\n return Math.abs(a - b) < 0.001;\n}\n\nconst MANIFEST_CLIP_ATTRS: ReadonlyArray<[string, (clip: ClipManifestClip) => number]> = [\n [\"data-start\", (clip) => clip.start],\n [\"data-duration\", (clip) => clip.duration],\n [\"data-track-index\", (clip) => clip.track],\n];\n\nfunction nodeMatchesClipTag(node: Element, clip: ClipManifestClip): boolean {\n return !clip.tagName || node.tagName.toLowerCase() === clip.tagName.toLowerCase();\n}\n\nfunction nodeMatchesManifestClip(node: Element, clip: ClipManifestClip): boolean {\n if (!nodeMatchesClipTag(node, clip)) return false;\n // An attribute only constrains the match when it parses to a finite number:\n // missing or garbled reads as \"unknown\", not \"mismatch\".\n return MANIFEST_CLIP_ATTRS.every(([attr, expected]) => {\n const actual = Number.parseFloat(node.getAttribute(attr) ?? \"\");\n return !Number.isFinite(actual) || numbersNearlyEqual(actual, expected(clip));\n });\n}\n\n/** Whether `node` sits in the composition the clip was read from, the chain the runtime records outermost first\n * (`resolveNearestCompositionContext` in core's runtime/timeline.ts). A clip without that scope accepts any node. */\nfunction nodeInClipScope(node: Element, clip: ClipManifestClip): boolean {\n const scope = clip.compositionAncestors;\n if (!scope) return true;\n const ids: string[] = [];\n for (let cursor = node.parentElement; cursor; cursor = cursor.parentElement) {\n const id = cursor.getAttribute(\"data-composition-id\");\n if (id) ids.unshift(id);\n }\n return ids.length === scope.length && ids.every((id, index) => id === scope[index]);\n}\n\n/** The first match in the clip's composition across `selectors`; a lone match stands only when none is in scope, as a\n * healed host can stale the clip's chain for a pass. An id can repeat in a sub-composition earlier in the document. */\nfunction findInClipScope(\n doc: Document,\n clip: ClipManifestClip,\n selectors: string[],\n): Element | null {\n let lone: Element | null = null;\n for (const selector of selectors) {\n const nodes = Array.from(doc.querySelectorAll(selector));\n const scoped = nodes.find((node) => nodeInClipScope(node, clip));\n if (scoped) return scoped;\n if (nodes.length === 1) lone ??= nodes[0];\n }\n return lone;\n}\n\nexport type PreviewTarget = Pick<TimelineElement, \"hfId\" | \"domId\" | \"id\" | \"sourceFile\">;\n\n/** Finds a row's preview element by `data-hf-id`, then id, preferring one in the row's own file: both repeat across\n * files. Indexes the document once, so a pass over every row costs one scan. */\nexport function previewElementFinder(\n doc: Document,\n selector = \"[data-hf-id], [id]\",\n): (target: PreviewTarget) => Element | null {\n const byKey = new Map<string, Element[]>();\n const add = (key: string, node: Element) => {\n const nodes = byKey.get(key);\n if (nodes) nodes.push(node);\n else byKey.set(key, [node]);\n };\n for (const node of doc.querySelectorAll(selector)) {\n const hfId = node.getAttribute(\"data-hf-id\");\n const id = node.getAttribute(\"id\");\n if (hfId) add(`hf:${hfId}`, node);\n if (id) add(`id:${id}`, node);\n }\n return (target) => {\n const matches = [\n ...((target.hfId && byKey.get(`hf:${target.hfId}`)) || []),\n ...(byKey.get(`id:${target.domId ?? target.id}`) ?? []),\n ];\n return (\n matches.find((node) => getTimelineElementSourceFile(node) === target.sourceFile) ??\n matches[0] ??\n null\n );\n };\n}\n\nexport function findClipElementById(doc: Document, clip: ClipManifestClip): Element | null {\n if (!clip.id) return null;\n const first = doc.getElementById(clip.id);\n if (!first || nodeInClipScope(first, clip)) return first;\n return findInClipScope(doc, clip, [`[id=\"${CSS.escape(clip.id)}\"]`]);\n}\n\nfunction findTimelineDomNode(doc: Document, clip: ClipManifestClip): Element | null {\n if (!clip.id) return null;\n const first = doc.getElementById(clip.id);\n if (first && nodeInClipScope(first, clip)) return first;\n const id = CSS.escape(clip.id);\n const byOtherKeys = [`[data-hf-id=\"${id}\"]`, `[data-composition-id=\"${id}\"]`, `.${id}`];\n return findInClipScope(doc, clip, first ? [`[id=\"${id}\"]`, ...byOtherKeys] : byOtherKeys);\n}\n\nexport function findTimelineDomNodeForClip(\n doc: Document,\n clip: ClipManifestClip,\n fallbackIndex: number,\n usedNodes = new Set<Element>(),\n getCandidates = () => getTimelineDomNodes(doc),\n): Element | null {\n const byIdentity = findTimelineDomNode(doc, clip);\n if (byIdentity && !usedNodes.has(byIdentity) && nodeMatchesClipTag(byIdentity, clip))\n return byIdentity;\n\n const candidates = getCandidates().filter((node) => !usedNodes.has(node));\n const exact = candidates.find((node) => nodeMatchesManifestClip(node, clip));\n if (exact) return exact;\n\n const positional = candidates[fallbackIndex];\n return positional && nodeMatchesClipTag(positional, clip) ? positional : null;\n}\n\n/** One synchronous hydration pass: snapshot only on a miss, never across reloads. */\nexport function createTimelineDomNodeResolver(doc: Document) {\n let candidates: Element[] | undefined;\n const usedNodes = new Set<Element>();\n const getCandidates = () => (candidates ??= getTimelineDomNodes(doc));\n return (clip: ClipManifestClip, index: number): Element | null => {\n const node = findTimelineDomNodeForClip(doc, clip, index, usedNodes, getCandidates);\n if (node) usedNodes.add(node);\n return node;\n };\n}\n","const PROJECT_HASH_PREFIX = \"#project/\";\n\nexport interface ProjectHashRoute {\n projectId: string;\n params: URLSearchParams;\n}\n\nconst WINDOWS_DRIVE_PREFIX = /^[a-z]:/i;\n\n/** Project names are single path segments, including when received from a hash or server. */\nexport function isValidProjectId(value: string): boolean {\n return (\n value.length > 0 &&\n value !== \".\" &&\n value !== \"..\" &&\n !WINDOWS_DRIVE_PREFIX.test(value) &&\n !value.includes(\"/\") &&\n !value.includes(\"\\\\\") &&\n !Array.from(value).some((char) => char.charCodeAt(0) < 32)\n );\n}\n\nfunction decodeHashProjectId(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeHashParams(\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): URLSearchParams {\n if (!params) return new URLSearchParams();\n if (params instanceof URLSearchParams) return params;\n\n const next = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (!key || value == null || value === \"\") continue;\n next.set(key, value);\n }\n return next;\n}\n\nexport function encodeProjectId(projectId: string): string {\n if (!isValidProjectId(projectId)) throw new Error(\"Invalid project ID\");\n return encodeURIComponent(projectId);\n}\n\nexport function buildProjectHash(\n projectId: string,\n params?: URLSearchParams | Record<string, string | null | undefined>,\n): string {\n const search = normalizeHashParams(params).toString();\n return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : \"\"}`;\n}\n\nexport function parseProjectHashRoute(hash: string): ProjectHashRoute | null {\n if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;\n\n const route = hash.slice(PROJECT_HASH_PREFIX.length);\n const queryIndex = route.indexOf(\"?\");\n const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;\n if (!encodedProjectId || encodedProjectId.includes(\"/\")) return null;\n\n const projectId = decodeHashProjectId(encodedProjectId);\n if (!isValidProjectId(projectId)) return null;\n\n const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : \"\";\n return {\n projectId,\n params: new URLSearchParams(rawParams),\n };\n}\n\nexport function parseProjectIdFromHash(hash: string): string | null {\n return parseProjectHashRoute(hash)?.projectId ?? null;\n}\n\nexport function buildProjectApiPath(projectId: string, suffix = \"\"): string {\n const normalizedSuffix = suffix && !suffix.startsWith(\"/\") ? `/${suffix}` : suffix;\n return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|