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:
ukimsanov
2026-07-13 16:48:52 -07:00
parent 19139b91ed
commit 84963ea8ba
14 changed files with 491 additions and 47 deletions
@@ -27,7 +27,7 @@ import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
import { useMountEffect } from "../../hooks/useMountEffect";
import { startOffCanvasIndicatorRefresh } from "./offCanvasIndicatorRefresh";
import { CanvasContextMenu } from "./CanvasContextMenu";
import type { ZOrderPatch } from "./canvasContextMenuZOrder";
import type { ZOrderAction, ZOrderPatch } from "./canvasContextMenuZOrder";
import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
// Re-exports for external consumers — preserving existing import paths.
@@ -91,12 +91,17 @@ interface DomEditOverlayProps {
*/
onDeleteSelection?: (selection: DomEditSelection) => void;
/**
* Called with the resolved z-order patch list after an optimistic DOM update.
* The patch list is tie-aware and may include sibling elements (see
* canvasContextMenuZOrder). Wire to handleDomZIndexReorderCommit from
* Called with the resolved z-order patch list and the menu action that
* produced it (feeds the undo coalesce key). The patch list is tie-aware and
* may include sibling elements (see canvasContextMenuZOrder); the live DOM is
* NOT yet mutated. Wire to handleDomZIndexReorderCommit from
* useDomEditActionsContext. See CanvasContextMenu.tsx module comment.
*/
onApplyZIndex?: (selection: DomEditSelection, patches: ZOrderPatch[]) => void;
onApplyZIndex?: (
selection: DomEditSelection,
patches: ZOrderPatch[],
action: ZOrderAction,
) => void;
}
// fallow-ignore-next-line complexity
@@ -562,8 +567,8 @@ export const DomEditOverlay = memo(function DomEditOverlay({
}
onApplyZIndex={
onApplyZIndex
? (patches) => {
onApplyZIndex(contextMenu.sel, patches);
? (patches, action) => {
onApplyZIndex(contextMenu.sel, patches, action);
}
: undefined
}