@hyperframes/studio 0.7.57 → 0.7.58
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--Z69cEkE.js → hyperframes-player-CtTDO63S.js} +1 -1
- package/dist/assets/{index-DfmYkU44.js → index-B_UvTX3E.js} +204 -204
- package/dist/assets/{index-KsfE1bUu.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-Bf1x1y8H.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +15 -4
- package/dist/index.html +2 -2
- package/dist/index.js +5954 -4474
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/LayersPanel.tsx +110 -99
- package/src/components/editor/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/hooks/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +143 -78
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +174 -81
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineEditing.test.tsx +302 -154
- package/src/hooks/useTimelineEditing.ts +97 -93
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +35 -3
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -28,7 +28,6 @@ import { useDomEditTextCommits } from "./useDomEditTextCommits";
|
|
|
28
28
|
import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
|
29
29
|
import { useElementLifecycleOps } from "./useElementLifecycleOps";
|
|
30
30
|
import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
|
|
31
|
-
import { readProjectFileContent } from "../utils/studioFileHistory";
|
|
32
31
|
|
|
33
32
|
// ── Helpers ──
|
|
34
33
|
|
|
@@ -67,9 +66,9 @@ function describeBatchPatchTarget(patch: DomEditPatchBatch["patches"][number]):
|
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
/**
|
|
70
|
-
* Surface server-reported unmatched patches. The
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* Surface server-reported unmatched patches. The server atomically refuses the
|
|
70
|
+
* whole multi-file gesture; the caller uses `durable: false` to roll back and
|
|
71
|
+
* reload, so report the refusal without turning it into a second failure.
|
|
73
72
|
*/
|
|
74
73
|
function reportUnmatchedBatchPatches(batch: DomEditPatchBatch, matched: boolean[]): void {
|
|
75
74
|
const unmatchedIds = batch.patches
|
|
@@ -78,7 +77,7 @@ function reportUnmatchedBatchPatches(batch: DomEditPatchBatch, matched: boolean[
|
|
|
78
77
|
if (unmatchedIds.length === 0) return;
|
|
79
78
|
console.warn(
|
|
80
79
|
`[studio] z-index reorder: server could not match ${unmatchedIds.length} patch target(s) in ` +
|
|
81
|
-
`${batch.sourceFile} (
|
|
80
|
+
`${batch.sourceFile} (the whole z-order gesture will revert on reload):`,
|
|
82
81
|
unmatchedIds.join(", "),
|
|
83
82
|
);
|
|
84
83
|
trackStudioSaveFailure({
|
|
@@ -89,40 +88,95 @@ function reportUnmatchedBatchPatches(batch: DomEditPatchBatch, matched: boolean[
|
|
|
89
88
|
});
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
interface AtomicElementPatchFile {
|
|
92
|
+
sourceFile: string;
|
|
93
|
+
changed: boolean;
|
|
94
|
+
matched?: boolean[];
|
|
95
|
+
before: string;
|
|
96
|
+
after: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class AtomicElementPatchConvergenceError extends Error {
|
|
100
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
101
|
+
super(message, options);
|
|
102
|
+
this.name = "AtomicElementPatchConvergenceError";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Keep the atomic response contract in one guard so callers do not compose validity.
|
|
107
|
+
// fallow-ignore-next-line complexity
|
|
108
|
+
function isAtomicElementPatchFile(value: unknown): value is AtomicElementPatchFile {
|
|
109
|
+
return (
|
|
110
|
+
typeof value === "object" &&
|
|
111
|
+
value !== null &&
|
|
112
|
+
"sourceFile" in value &&
|
|
113
|
+
typeof value.sourceFile === "string" &&
|
|
114
|
+
"changed" in value &&
|
|
115
|
+
typeof value.changed === "boolean" &&
|
|
116
|
+
(!("matched" in value) ||
|
|
117
|
+
(Array.isArray(value.matched) &&
|
|
118
|
+
value.matched.every((matched) => typeof matched === "boolean"))) &&
|
|
119
|
+
"before" in value &&
|
|
120
|
+
typeof value.before === "string" &&
|
|
121
|
+
"after" in value &&
|
|
122
|
+
typeof value.after === "string" &&
|
|
123
|
+
value.changed === (value.before !== value.after)
|
|
101
124
|
);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// This is the single client owner for dispatching and validating the aggregate
|
|
128
|
+
// atomic endpoint. Splitting validation from the request would weaken that wire contract.
|
|
129
|
+
// fallow-ignore-next-line complexity
|
|
130
|
+
async function patchElementBatches(projectId: string, batches: DomEditPatchBatch[]) {
|
|
131
|
+
const body = JSON.stringify({ batches });
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(
|
|
134
|
+
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
|
|
135
|
+
{
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
body,
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const rejection = await readErrorResponseBody(response);
|
|
143
|
+
throw new StudioSaveHttpError(formatPatchRejectionMessage(rejection), response.status);
|
|
144
|
+
}
|
|
145
|
+
const result: unknown = await response.json().catch(() => null);
|
|
146
|
+
if (
|
|
147
|
+
typeof result !== "object" ||
|
|
148
|
+
result === null ||
|
|
149
|
+
!("durable" in result) ||
|
|
150
|
+
typeof result.durable !== "boolean" ||
|
|
151
|
+
!("files" in result) ||
|
|
152
|
+
!Array.isArray(result.files) ||
|
|
153
|
+
result.files.length !== batches.length ||
|
|
154
|
+
!result.files.every(isAtomicElementPatchFile) ||
|
|
155
|
+
(!result.durable && result.files.some((file) => file.changed))
|
|
156
|
+
) {
|
|
157
|
+
throw new StudioSaveHttpError("Invalid atomic element patch response", 502);
|
|
158
|
+
}
|
|
159
|
+
const files = result.files.map((file, index) => {
|
|
160
|
+
const batch = batches[index];
|
|
161
|
+
const matched = file.matched ?? [];
|
|
162
|
+
if (
|
|
163
|
+
!batch ||
|
|
164
|
+
file.sourceFile !== batch.sourceFile ||
|
|
165
|
+
(matched.length !== 0 && matched.length !== batch.patches.length)
|
|
166
|
+
) {
|
|
167
|
+
throw new StudioSaveHttpError("Invalid atomic element patch response", 502);
|
|
168
|
+
}
|
|
169
|
+
reportUnmatchedBatchPatches(batch, matched);
|
|
170
|
+
return {
|
|
171
|
+
...file,
|
|
172
|
+
matched,
|
|
173
|
+
allMatched: matched.length === batch.patches.length && matched.every(Boolean),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
return { durable: result.durable, files };
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw new AtomicElementPatchConvergenceError(getErrorDetail(error), { cause: error });
|
|
105
179
|
}
|
|
106
|
-
const result = (await response.json()) as {
|
|
107
|
-
changed?: boolean;
|
|
108
|
-
matched?: boolean[];
|
|
109
|
-
content?: string;
|
|
110
|
-
};
|
|
111
|
-
if (Array.isArray(result.matched)) reportUnmatchedBatchPatches(batch, result.matched);
|
|
112
|
-
return {
|
|
113
|
-
sourceFile: batch.sourceFile,
|
|
114
|
-
changed: result.changed === true,
|
|
115
|
-
// Skip-reload safety: the persist is only provably in sync with the live
|
|
116
|
-
// DOM when the server confirmed EVERY patch target matched. A missing /
|
|
117
|
-
// short matched[] is treated as unknown (false) so the caller falls back
|
|
118
|
-
// to reloading rather than silently diverging from disk.
|
|
119
|
-
allMatched:
|
|
120
|
-
Array.isArray(result.matched) &&
|
|
121
|
-
result.matched.length === batch.patches.length &&
|
|
122
|
-
result.matched.every(Boolean),
|
|
123
|
-
before,
|
|
124
|
-
after: typeof result.content === "string" ? result.content : before,
|
|
125
|
-
};
|
|
126
180
|
}
|
|
127
181
|
|
|
128
182
|
/**
|
|
@@ -142,7 +196,7 @@ export interface UseDomEditCommitsParams {
|
|
|
142
196
|
activeCompPath: string | null;
|
|
143
197
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
144
198
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
145
|
-
queueDomEditSave: (save: () => Promise<
|
|
199
|
+
queueDomEditSave: <T>(save: () => Promise<T>) => Promise<T>;
|
|
146
200
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
147
201
|
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
148
202
|
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
@@ -382,48 +436,59 @@ export function useDomEditCommits({
|
|
|
382
436
|
|
|
383
437
|
const commitDomEditPatchBatches: CommitDomEditPatchBatches = useCallback(
|
|
384
438
|
(batches, options) =>
|
|
385
|
-
queueDomEditSave(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
`DOM patch contains unsafe values: ${formatUnsafeFieldList(unsafeFields)}`,
|
|
395
|
-
{ alreadyToasted: true },
|
|
439
|
+
queueDomEditSave(
|
|
440
|
+
// One queued transaction owns validation, persistence, history, reload,
|
|
441
|
+
// and its durable result; splitting those phases risks partial commits.
|
|
442
|
+
// fallow-ignore-next-line complexity
|
|
443
|
+
async () => {
|
|
444
|
+
const pid = projectIdRef.current;
|
|
445
|
+
if (!pid) throw new Error("No active project");
|
|
446
|
+
const unsafeFields = batches.flatMap((batch) =>
|
|
447
|
+
batch.patches.flatMap((patch) => findUnsafeDomPatchValues(patch)),
|
|
396
448
|
);
|
|
397
|
-
|
|
449
|
+
if (unsafeFields.length > 0) {
|
|
450
|
+
showToast("Couldn't save edit because it contains invalid layout values", "error");
|
|
451
|
+
throw new DomEditPersistUnsafeValueError(
|
|
452
|
+
`DOM patch contains unsafe values: ${formatUnsafeFieldList(unsafeFields)}`,
|
|
453
|
+
{ alreadyToasted: true },
|
|
454
|
+
);
|
|
455
|
+
}
|
|
398
456
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
457
|
+
domEditSaveTimestampRef.current = Date.now();
|
|
458
|
+
const atomicResult = await patchElementBatches(pid, batches);
|
|
459
|
+
const allMatched =
|
|
460
|
+
atomicResult.durable && atomicResult.files.every((result) => result.allMatched);
|
|
461
|
+
const files = Object.fromEntries(
|
|
462
|
+
atomicResult.files
|
|
463
|
+
.filter((result) => result.changed)
|
|
464
|
+
.map((result) => [result.sourceFile, { before: result.before, after: result.after }]),
|
|
465
|
+
);
|
|
466
|
+
const changed = Object.keys(files).length > 0;
|
|
467
|
+
if (changed) {
|
|
468
|
+
await editHistory.recordEdit({
|
|
469
|
+
label: options.label,
|
|
470
|
+
kind: "manual",
|
|
471
|
+
coalesceKey: options.coalesceKey,
|
|
472
|
+
coalesceMs: options.coalesceMs,
|
|
473
|
+
files,
|
|
474
|
+
});
|
|
475
|
+
forceReloadSdkSession?.();
|
|
476
|
+
}
|
|
477
|
+
const durable = allMatched;
|
|
478
|
+
// A z-only reorder already applied its inline styles to the live iframe
|
|
479
|
+
// DOM (and the store) synchronously, so remounting the iframe here only
|
|
480
|
+
// produces a visible blink. Skip the reload when the caller asked for it
|
|
481
|
+
// AND the persist is provably in sync: style-only ops, every target
|
|
482
|
+
// matched. Any unmatched patch means the live DOM now shows state disk
|
|
483
|
+
// doesn't hold — reload so the preview reconverges. (The SSE/file-watcher
|
|
484
|
+
// reload is independently suppressed by domEditSaveTimestampRef above.)
|
|
485
|
+
const skipSafe =
|
|
486
|
+
options.skipReload === true && batchesAreInlineStyleOnly(batches) && durable;
|
|
487
|
+
if (!durable || (changed && !skipSafe)) reloadPreview();
|
|
488
|
+
return { durable, allMatched, changed };
|
|
489
|
+
},
|
|
490
|
+
).catch((error) => {
|
|
491
|
+
if (error instanceof AtomicElementPatchConvergenceError) reloadPreview();
|
|
427
492
|
const alreadyToasted =
|
|
428
493
|
(error instanceof StudioSaveHttpError ||
|
|
429
494
|
error instanceof DomEditPersistUnsafeValueError) &&
|
|
@@ -231,7 +231,9 @@ describe("onReorderShadow source filter", () => {
|
|
|
231
231
|
setRightPanelTab: vi.fn(),
|
|
232
232
|
showToast: vi.fn(),
|
|
233
233
|
refreshPreviewDocumentVersion: vi.fn(),
|
|
234
|
-
queueDomEditSave: vi.fn(async (save: () => Promise<
|
|
234
|
+
queueDomEditSave: vi.fn(async <T,>(save: () => Promise<T>) => save()) as <T>(
|
|
235
|
+
save: () => Promise<T>,
|
|
236
|
+
) => Promise<T>,
|
|
235
237
|
readProjectFile,
|
|
236
238
|
writeProjectFile: vi.fn(async () => {}),
|
|
237
239
|
updateEditingFileContent: vi.fn(),
|
|
@@ -43,7 +43,7 @@ export interface UseDomEditSessionParams {
|
|
|
43
43
|
setRightPanelTab: (tab: RightPanelTab) => void;
|
|
44
44
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
45
45
|
refreshPreviewDocumentVersion: () => void;
|
|
46
|
-
queueDomEditSave: (save: () => Promise<
|
|
46
|
+
queueDomEditSave: <T>(save: () => Promise<T>) => Promise<T>;
|
|
47
47
|
readProjectFile: (path: string) => Promise<string>;
|
|
48
48
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
49
49
|
updateEditingFileContent: (path: string, content: string) => void;
|