@hyperframes/studio 0.7.61 → 0.7.63
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 → hyperframes-player-BO2UXx-E.js} +1 -1
- package/dist/assets/{index-D-uFclFj.js → index-C0A0DlLk.js} +1 -1
- package/dist/assets/index-C_7k3kwr.js +426 -0
- package/dist/assets/index-Ceyz8Qt2.css +1 -0
- package/dist/assets/{index-BCpoiv9S.js → index-_72wCKSP.js} +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.html +2 -2
- package/dist/index.js +3504 -2955
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +15 -10
- package/src/components/EditorShell.tsx +12 -0
- package/src/components/StudioLeftSidebar.tsx +3 -0
- package/src/components/editor/DomEditSelectionChrome.test.tsx +51 -0
- package/src/components/editor/PropertyPanelFlat.tsx +6 -0
- package/src/components/editor/compositionReliabilityFixture.integration.test.ts +105 -0
- package/src/components/editor/domEditing.test.ts +34 -0
- package/src/components/editor/propertyPanelColor.test.tsx +31 -0
- package/src/components/editor/propertyPanelColor.tsx +150 -28
- package/src/components/editor/propertyPanelCommitField.tsx +178 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +34 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +3 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +5 -1
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +6 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +47 -4
- package/src/components/editor/propertyPanelFlatTextSection.tsx +8 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +4 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +3 -100
- package/src/components/editor/propertyPanelSections.tsx +2 -2
- package/src/components/editor/propertyPanelTypes.ts +2 -0
- package/src/components/editor/useInspectorGestureTransaction.test.tsx +37 -0
- package/src/components/editor/useInspectorGestureTransaction.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +6 -0
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +87 -0
- package/src/components/sidebar/CompositionsTab.tsx +45 -2
- package/src/components/sidebar/LeftSidebar.tsx +3 -0
- package/src/contexts/StudioContext.tsx +4 -0
- package/src/hooks/timelineEditingHelpers.ts +10 -4
- package/src/hooks/usePreviewPersistence.ts +7 -1
- package/src/hooks/useRazorSplit.history.test.tsx +21 -28
- package/src/hooks/useRazorSplit.test.tsx +20 -9
- package/src/hooks/useRazorSplit.testHelpers.ts +34 -22
- package/src/hooks/useRazorSplit.ts +78 -350
- package/src/hooks/useTimelineAddAtPlayhead.ts +15 -0
- package/src/hooks/useTimelineAssetDropOps.ts +52 -2
- package/src/hooks/useTimelineEditing.ts +19 -17
- package/src/hooks/useTimelineGroupEditing.ts +6 -7
- package/src/player/components/Timeline.tsx +12 -1
- package/src/player/components/timelineAuthoredMoveTarget.ts +77 -0
- package/src/player/components/timelineAuthoredTrack.ts +29 -0
- package/src/player/components/timelineCallbacks.ts +4 -0
- package/src/player/components/timelineClipDragCommit.test.ts +176 -0
- package/src/player/components/timelineClipDragCommit.ts +77 -105
- package/src/player/components/timelineClipDragPreview.test.ts +33 -1
- package/src/player/components/timelineClipDragPreview.ts +2 -1
- package/src/player/components/timelineCollision.test.ts +18 -0
- package/src/player/components/timelineCollision.ts +12 -9
- package/src/player/components/timelineDragDrop.ts +28 -7
- package/src/player/components/timelineEditCapabilities.ts +4 -1
- package/src/player/components/timelineGroupEditing.test.ts +10 -0
- package/src/player/components/timelineGroupEditing.ts +4 -3
- package/src/player/components/timelineLaneMoveRefresh.ts +14 -0
- package/src/player/components/timelineLayout.ts +3 -1
- package/src/player/components/timelineZMirror.ts +1 -1
- package/src/player/components/useTimelineClipDrag.ts +5 -0
- package/src/player/components/useTimelineEditPinning.ts +12 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +42 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/lib/playbackTypes.ts +2 -0
- package/src/player/lib/timelineDOM.test.ts +58 -0
- package/src/player/lib/timelineDOM.ts +18 -1
- package/src/player/lib/timelineElementHelpers.ts +21 -11
- package/src/player/store/playerStore.ts +2 -1
- package/src/utils/razorSplitTransaction.test.ts +175 -0
- package/src/utils/razorSplitTransaction.ts +200 -0
- package/src/utils/studioPreviewHelpers.test.ts +27 -0
- package/src/utils/timelineCompositionDrop.test.ts +12 -0
- package/src/utils/timelineCompositionDrop.ts +16 -0
- package/src/utils/timelineCompositionInsert.test.ts +142 -0
- package/src/utils/timelineCompositionInsert.ts +90 -0
- package/src/utils/timelineElementSplit.test.ts +14 -2
- package/src/utils/timelineElementSplit.ts +5 -1
- package/dist/assets/index-2mxh_HSy.js +0 -426
- package/dist/assets/index-BhWig0mx.css +0 -1
|
@@ -166,6 +166,14 @@ export function patchIframeDomTiming(
|
|
|
166
166
|
// Cross-origin or mid-navigation — file save is enqueued; iframe patch is best-effort.
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
+
|
|
170
|
+
export function playbackStartAttributeForElement(
|
|
171
|
+
element: Pick<TimelineElement, "kind" | "playbackStartAttr">,
|
|
172
|
+
): "data-media-start" | "data-playback-start" {
|
|
173
|
+
return element.playbackStartAttr === "playback-start" || element.kind === "composition"
|
|
174
|
+
? "data-playback-start"
|
|
175
|
+
: "data-media-start";
|
|
176
|
+
}
|
|
169
177
|
// fallow-ignore-next-line complexity
|
|
170
178
|
function resolveResizePlaybackStart(
|
|
171
179
|
original: string,
|
|
@@ -174,8 +182,7 @@ function resolveResizePlaybackStart(
|
|
|
174
182
|
updates: Pick<TimelineElement, "start" | "playbackStart">,
|
|
175
183
|
): { attrName: string; value: number } | null {
|
|
176
184
|
if (updates.playbackStart != null) {
|
|
177
|
-
const attrName =
|
|
178
|
-
element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
|
|
185
|
+
const attrName = playbackStartAttributeForElement(element).slice("data-".length);
|
|
179
186
|
return { attrName, value: updates.playbackStart };
|
|
180
187
|
}
|
|
181
188
|
const trimDelta = updates.start - element.start;
|
|
@@ -185,8 +192,7 @@ function resolveResizePlaybackStart(
|
|
|
185
192
|
readAttributeByTarget(original, target, "media-start");
|
|
186
193
|
const current = raw != null ? parseFloat(raw) : undefined;
|
|
187
194
|
if (current == null || !Number.isFinite(current)) return null;
|
|
188
|
-
const attrName =
|
|
189
|
-
element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
|
|
195
|
+
const attrName = playbackStartAttributeForElement(element).slice("data-".length);
|
|
190
196
|
return {
|
|
191
197
|
attrName,
|
|
192
198
|
value: Math.max(0, current + trimDelta * Math.max(element.playbackRate ?? 1, 0.1)),
|
|
@@ -208,13 +208,19 @@ export function usePreviewPersistence({
|
|
|
208
208
|
// attributes onto the live DOM and re-runs the timeline at the SAME playhead,
|
|
209
209
|
// falling back to reloadPreview for anything structural (split/delete undo),
|
|
210
210
|
// multi-file, sub-comp, or a permanent soft-reload failure.
|
|
211
|
-
applyUndoRestoreToPreview(
|
|
211
|
+
const strategy = applyUndoRestoreToPreview(
|
|
212
212
|
previewIframeRef.current,
|
|
213
213
|
activeCompPathRef.current,
|
|
214
214
|
restore.files,
|
|
215
215
|
usePlayerStore.getState().currentTime,
|
|
216
216
|
reloadPreview,
|
|
217
217
|
);
|
|
218
|
+
if (strategy === "full") {
|
|
219
|
+
const player = usePlayerStore.getState();
|
|
220
|
+
player.setElements([]);
|
|
221
|
+
player.setSelectedElementId(null);
|
|
222
|
+
player.setTimelineReady(false);
|
|
223
|
+
}
|
|
218
224
|
},
|
|
219
225
|
[previewIframeRef, activeCompPathRef, reloadPreview],
|
|
220
226
|
);
|
|
@@ -67,43 +67,36 @@ function mountRazorSplit(opts: { gsap?: boolean; previewStamp?: boolean } = {}):
|
|
|
67
67
|
onChange: () => {},
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
// Faithful stand-in for the
|
|
71
|
-
//
|
|
70
|
+
// Faithful stand-in for the atomic server cut: one forward file write and one
|
|
71
|
+
// response carrying the canonical history snapshots.
|
|
72
72
|
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
|
73
73
|
const u = String(url);
|
|
74
|
-
if (u.includes("/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
disk["index.html"] = SPLIT_GSAP;
|
|
79
|
-
const version = `"test-gsap-${SPLIT_GSAP.length}"`;
|
|
80
|
-
return new Response(JSON.stringify({ ok: true, after: SPLIT_GSAP, version }), {
|
|
81
|
-
status: 200,
|
|
82
|
-
headers: { "Content-Type": "application/json", ETag: version },
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
// The fixture has no GSAP script — mirror the server's 400 response.
|
|
86
|
-
return new Response(JSON.stringify({ error: "no GSAP script found in file" }), {
|
|
87
|
-
status: 400,
|
|
88
|
-
headers: { "Content-Type": "application/json" },
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
if (u.includes("/file-mutations/split-element/")) {
|
|
92
|
-
disk["index.html"] = SPLIT;
|
|
93
|
-
const version = `"test-split-${SPLIT.length}"`;
|
|
74
|
+
if (u.includes("/file-mutations/split-batch")) {
|
|
75
|
+
const before = disk["index.html"];
|
|
76
|
+
disk["index.html"] = finalContent;
|
|
77
|
+
const version = `"test-cut-${finalContent.length}"`;
|
|
94
78
|
return new Response(
|
|
95
79
|
JSON.stringify({
|
|
96
80
|
ok: true,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
81
|
+
outcome: "committed",
|
|
82
|
+
files: [
|
|
83
|
+
{
|
|
84
|
+
path: "index.html",
|
|
85
|
+
before,
|
|
86
|
+
after: finalContent,
|
|
87
|
+
version,
|
|
88
|
+
writeToken: "test-cut",
|
|
89
|
+
splitCount: 1,
|
|
90
|
+
skippedSelectors: [],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
101
93
|
}),
|
|
102
|
-
{ status: 200, headers: { "Content-Type": "application/json"
|
|
94
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
103
95
|
);
|
|
104
96
|
}
|
|
105
97
|
if (u.includes("/files/")) {
|
|
106
|
-
|
|
98
|
+
const content = disk["index.html"];
|
|
99
|
+
return new Response(JSON.stringify({ content, version: `"test-${content.length}"` }), {
|
|
107
100
|
status: 200,
|
|
108
101
|
headers: { "Content-Type": "application/json" },
|
|
109
102
|
});
|
|
@@ -17,19 +17,30 @@ function jsonResponse(body: unknown, status = 200): Response {
|
|
|
17
17
|
describe("useRazorSplit mutation versions", () => {
|
|
18
18
|
afterEach(() => vi.restoreAllMocks());
|
|
19
19
|
|
|
20
|
-
it("observes
|
|
20
|
+
it("observes the batch version without a redundant client forward write", async () => {
|
|
21
21
|
const original = '<div id="clip" data-start="0" data-duration="4">Clip</div>';
|
|
22
22
|
const htmlSplit =
|
|
23
23
|
'<div id="clip" data-start="0" data-duration="2">Clip</div><div id="clip-split" data-start="2" data-duration="2">Clip</div>';
|
|
24
24
|
const final = `${htmlSplit}<script>window.__timelines = {}</script>`;
|
|
25
25
|
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
|
|
26
26
|
const url = String(input);
|
|
27
|
-
if (url.includes("/files/")) return jsonResponse({ content: original });
|
|
28
|
-
if (url.includes("/file-mutations/split-
|
|
29
|
-
return jsonResponse({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
if (url.includes("/files/")) return jsonResponse({ content: original, version: '"v0"' });
|
|
28
|
+
if (url.includes("/file-mutations/split-batch")) {
|
|
29
|
+
return jsonResponse({
|
|
30
|
+
ok: true,
|
|
31
|
+
outcome: "committed",
|
|
32
|
+
files: [
|
|
33
|
+
{
|
|
34
|
+
path: "index.html",
|
|
35
|
+
before: original,
|
|
36
|
+
after: final,
|
|
37
|
+
version: '"v-cut"',
|
|
38
|
+
writeToken: "cut-1",
|
|
39
|
+
splitCount: 1,
|
|
40
|
+
skippedSelectors: [],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
});
|
|
33
44
|
}
|
|
34
45
|
throw new Error(`Unexpected request: ${url}`);
|
|
35
46
|
});
|
|
@@ -78,8 +89,8 @@ describe("useRazorSplit mutation versions", () => {
|
|
|
78
89
|
);
|
|
79
90
|
});
|
|
80
91
|
|
|
81
|
-
expect(order).toEqual(['observe:index.html:"v-
|
|
82
|
-
expect(writeProjectFile).
|
|
92
|
+
expect(order).toEqual(['observe:index.html:"v-cut"']);
|
|
93
|
+
expect(writeProjectFile).not.toHaveBeenCalled();
|
|
83
94
|
expect(recordEdit).toHaveBeenCalledTimes(1);
|
|
84
95
|
|
|
85
96
|
act(() => root.unmount());
|
|
@@ -8,15 +8,19 @@ export interface SplitBody {
|
|
|
8
8
|
elementDuration: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface BatchFileBody {
|
|
12
|
+
path: string;
|
|
13
|
+
targets: SplitBody[];
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
function decodePathFromUrl(url: string, marker: string): string {
|
|
12
17
|
const encoded = url.slice(url.indexOf(marker) + marker.length);
|
|
13
18
|
return decodeURIComponent(encoded);
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
|
-
* Fetch mock shared by both harnesses:
|
|
18
|
-
*
|
|
19
|
-
* echo the in-memory `disk`. `onSplit` (when set) records each split request's body.
|
|
22
|
+
* Fetch mock shared by both harnesses: the atomic split batch writes each file
|
|
23
|
+
* once and returns its canonical snapshots; file reads echo the in-memory disk.
|
|
20
24
|
*/
|
|
21
25
|
export function createSplitFetchMock(
|
|
22
26
|
disk: Record<string, string>,
|
|
@@ -24,31 +28,39 @@ export function createSplitFetchMock(
|
|
|
24
28
|
) {
|
|
25
29
|
return vi.fn(async (url: string, init?: RequestInit) => {
|
|
26
30
|
const u = String(url);
|
|
27
|
-
if (u.includes("/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
if (u.includes("/file-mutations/split-batch")) {
|
|
32
|
+
const body = JSON.parse(String(init?.body)) as { files: BatchFileBody[] };
|
|
33
|
+
const files = body.files.map((file) => {
|
|
34
|
+
const before = disk[file.path];
|
|
35
|
+
for (const target of file.targets) onSplit?.(file.path, target);
|
|
36
|
+
const after = `${before}${"<!--split-->".repeat(file.targets.length)}`;
|
|
37
|
+
const version = `"test-${file.path}-${after.length}"`;
|
|
38
|
+
return {
|
|
39
|
+
path: file.path,
|
|
40
|
+
before,
|
|
41
|
+
after,
|
|
42
|
+
version,
|
|
43
|
+
writeToken: "test-cut",
|
|
44
|
+
splitCount: file.targets.length,
|
|
45
|
+
skippedSelectors: [],
|
|
46
|
+
};
|
|
32
47
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const path = decodePathFromUrl(u, "/file-mutations/split-element/");
|
|
36
|
-
onSplit?.(path, JSON.parse(String(init?.body)) as SplitBody);
|
|
37
|
-
// Return content that differs from the original so `changed` is true.
|
|
38
|
-
const after = `${disk[path]}<!--split-->`;
|
|
39
|
-
const version = `"test-${path}-${after.length}"`;
|
|
40
|
-
disk[path] = after; // server writes the split to disk
|
|
41
|
-
return new Response(JSON.stringify({ ok: true, changed: true, content: after, version }), {
|
|
48
|
+
for (const file of files) disk[file.path] = file.after;
|
|
49
|
+
return new Response(JSON.stringify({ ok: true, outcome: "committed", files }), {
|
|
42
50
|
status: 200,
|
|
43
|
-
headers: { "Content-Type": "application/json"
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
44
52
|
});
|
|
45
53
|
}
|
|
46
54
|
if (u.includes("/files/")) {
|
|
47
55
|
const path = decodePathFromUrl(u, "/files/").replace(/\?.*$/, "");
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
const content = disk[path] ?? "";
|
|
57
|
+
return new Response(
|
|
58
|
+
JSON.stringify({ content, version: `"test-${path}-${content.length}"` }),
|
|
59
|
+
{
|
|
60
|
+
status: 200,
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
},
|
|
63
|
+
);
|
|
52
64
|
}
|
|
53
65
|
void init;
|
|
54
66
|
throw new Error(`unexpected fetch: ${u}`);
|