mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(studio): persist canvas z-order actions correctly for static elements
An adversarial review of the canvas context-menu z-order pipeline (Bring to Front / Forward / Backward / Send to Back) found the resolver math sound but the glue between the menu and the commit hook broken: - The menu optimistically wrote style.zIndex AND position: relative to the live elements BEFORE the commit hook ran. The hook decides whether to persist position by checking getComputedStyle(el).position === 'static' — always false after the pre-apply — so the position patch was never persisted on the menu path and the reorder silently reverted at the post-commit reload for any nested/static element (root clips survive only because the runtime forces position:absolute). The same pre-apply made the failure rollback capture the already-mutated values, restoring the broken state on persist errors. The menu no longer pre-applies; the hook owns the live writes (it already applied both synchronously) and now sees true priors. Siblings without a persistable identity still get their z applied live-only so a renumber stays visually coherent. - The commit hook's entry.key store-sync plumbing had zero production callers; the store zIndex went stale until full reload. All three callers (canvas menu via PreviewOverlays, timeline lane z-sync, LayersPanel) now derive and pass the timeline store key (new deriveTimelineStoreKey helper). - patchElementBatch discarded the server's per-patch matched[]; unresolvable siblings persisted partially and silently. Unmatched targets now warn and report save-failure telemetry (z-reorder-unmatched) without rolling back the matched subset. - template/noscript elements counted as painting siblings, so renumber fallbacks wrote z-index/position into <template> tags in the source file. Excluded from the sibling family. - The default undo coalesce key merged DISTINCT z actions within 300ms into one undo entry; the action kind is now part of the key (LayersPanel drags keep coalescing within a drag; explicit lane-move gesture keys untouched). - rectsIntersect comment claimed touching rects intersect; the strict inequalities say otherwise — comment fixed.
This commit is contained in:
@@ -61,6 +61,34 @@ interface RecordEditInput {
|
||||
files: Record<string, { before: string; after: string }>;
|
||||
}
|
||||
|
||||
/** Human-readable identifier for a batch patch target (for the unmatched warning). */
|
||||
function describeBatchPatchTarget(patch: DomEditPatchBatch["patches"][number]): string {
|
||||
return patch.target.id ?? patch.target.hfId ?? patch.target.selector ?? "(unaddressed)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface server-reported unmatched patches. The matched subset already
|
||||
* persisted, so this must NOT throw (a throw would roll back applied state) —
|
||||
* warn and emit save-failure telemetry with a distinct reason instead.
|
||||
*/
|
||||
function reportUnmatchedBatchPatches(batch: DomEditPatchBatch, matched: boolean[]): void {
|
||||
const unmatchedIds = batch.patches
|
||||
.filter((_, index) => matched[index] === false)
|
||||
.map(describeBatchPatchTarget);
|
||||
if (unmatchedIds.length === 0) return;
|
||||
console.warn(
|
||||
`[studio] z-index reorder: server could not match ${unmatchedIds.length} patch target(s) in ` +
|
||||
`${batch.sourceFile} (their z-order will revert on reload):`,
|
||||
unmatchedIds.join(", "),
|
||||
);
|
||||
trackStudioSaveFailure({
|
||||
source: "dom_edit",
|
||||
error: new Error(`Batch patch target(s) unmatched: ${unmatchedIds.join(", ")}`),
|
||||
filePath: batch.sourceFile,
|
||||
mutationType: "z-reorder-unmatched",
|
||||
});
|
||||
}
|
||||
|
||||
async function patchElementBatch(projectId: string, batch: DomEditPatchBatch) {
|
||||
const before = await readProjectFileContent(projectId, batch.sourceFile);
|
||||
const response = await fetch(
|
||||
@@ -77,8 +105,10 @@ async function patchElementBatch(projectId: string, batch: DomEditPatchBatch) {
|
||||
}
|
||||
const result = (await response.json()) as {
|
||||
changed?: boolean;
|
||||
matched?: boolean[];
|
||||
content?: string;
|
||||
};
|
||||
if (Array.isArray(result.matched)) reportUnmatchedBatchPatches(batch, result.matched);
|
||||
return {
|
||||
sourceFile: batch.sourceFile,
|
||||
changed: result.changed === true,
|
||||
|
||||
Reference in New Issue
Block a user