diff --git a/packages/studio/src/components/editor/LayersPanel.test.ts b/packages/studio/src/components/editor/LayersPanel.test.ts new file mode 100644 index 000000000..f34db36f0 --- /dev/null +++ b/packages/studio/src/components/editor/LayersPanel.test.ts @@ -0,0 +1,135 @@ +// @vitest-environment happy-dom + +import { describe, expect, it } from "vitest"; +import { Window } from "happy-dom"; +import type { DomEditLayerItem } from "./domEditingTypes"; +import { sortLayersByZIndex } from "./LayersPanel"; +import { isLayerDraggable } from "./useLayerDrag"; + +function makeLayer( + overrides: Partial & { zIndex?: string; locked?: boolean }, +): DomEditLayerItem { + const win = new Window(); + const doc = win.document; + const parent = doc.createElement("div") as unknown as HTMLElement; + if (overrides.locked) { + (parent as unknown as Element).setAttribute("data-timeline-locked", "true"); + } + const el = doc.createElement("div") as unknown as HTMLElement; + parent.appendChild(el as unknown as Node); + if (overrides.zIndex != null) { + (el as unknown as { style: { zIndex: string } }).style.zIndex = overrides.zIndex; + } + if (overrides.id) { + (el as unknown as Element).setAttribute("id", overrides.id); + } + return { + key: overrides.key ?? `layer-${Math.random()}`, + element: el, + label: overrides.label ?? "div", + tagName: overrides.tagName ?? "div", + depth: overrides.depth ?? 0, + childCount: overrides.childCount ?? 0, + id: overrides.id, + selector: overrides.selector, + selectorIndex: overrides.selectorIndex, + sourceFile: overrides.sourceFile ?? "index.html", + }; +} + +describe("sortLayersByZIndex", () => { + it("sorts siblings by z-index descending", () => { + const a = makeLayer({ key: "a", zIndex: "1", depth: 0 }); + const b = makeLayer({ key: "b", zIndex: "3", depth: 0 }); + const c = makeLayer({ key: "c", zIndex: "2", depth: 0 }); + + const sorted = sortLayersByZIndex([a, b, c]); + expect(sorted.map((l) => l.key)).toEqual(["b", "c", "a"]); + }); + + it("preserves DOM order (reversed) for siblings with auto z-index", () => { + const a = makeLayer({ key: "a", depth: 0 }); + const b = makeLayer({ key: "b", depth: 0 }); + const c = makeLayer({ key: "c", depth: 0 }); + + const sorted = sortLayersByZIndex([a, b, c]); + expect(sorted.map((l) => l.key)).toEqual(["c", "b", "a"]); + }); + + it("sorts explicit z-index above auto, auto elements maintain reversed DOM order", () => { + const a = makeLayer({ key: "a", depth: 0 }); + const b = makeLayer({ key: "b", zIndex: "5", depth: 0 }); + const c = makeLayer({ key: "c", depth: 0 }); + + const sorted = sortLayersByZIndex([a, b, c]); + expect(sorted.map((l) => l.key)).toEqual(["b", "c", "a"]); + }); + + it("sorts children independently of their parent's siblings", () => { + const parent1 = makeLayer({ key: "p1", zIndex: "1", depth: 0, childCount: 2 }); + const child1a = makeLayer({ key: "c1a", zIndex: "3", depth: 1 }); + const child1b = makeLayer({ key: "c1b", zIndex: "1", depth: 1 }); + const parent2 = makeLayer({ key: "p2", zIndex: "2", depth: 0, childCount: 1 }); + const child2a = makeLayer({ key: "c2a", zIndex: "1", depth: 1 }); + + const sorted = sortLayersByZIndex([parent1, child1a, child1b, parent2, child2a]); + expect(sorted.map((l) => l.key)).toEqual(["p2", "c2a", "p1", "c1a", "c1b"]); + }); + + it("handles single-element groups without crash", () => { + const single = makeLayer({ key: "only", zIndex: "5", depth: 0 }); + const sorted = sortLayersByZIndex([single]); + expect(sorted).toEqual([single]); + }); + + it("returns empty array for empty input", () => { + expect(sortLayersByZIndex([])).toEqual([]); + }); + + it("handles duplicate z-index values with reverse DOM order tiebreak", () => { + const a = makeLayer({ key: "a", zIndex: "2", depth: 0 }); + const b = makeLayer({ key: "b", zIndex: "1", depth: 0 }); + const c = makeLayer({ key: "c", zIndex: "2", depth: 0 }); + + const sorted = sortLayersByZIndex([a, b, c]); + expect(sorted.map((l) => l.key)).toEqual(["c", "a", "b"]); + }); + + it("preserves deeply nested structure with sorting at each level", () => { + const root = makeLayer({ key: "root", depth: 0, childCount: 2 }); + const a = makeLayer({ key: "a", zIndex: "1", depth: 1, childCount: 2 }); + const a1 = makeLayer({ key: "a1", zIndex: "10", depth: 2 }); + const a2 = makeLayer({ key: "a2", zIndex: "20", depth: 2 }); + const b = makeLayer({ key: "b", zIndex: "2", depth: 1 }); + + const sorted = sortLayersByZIndex([root, a, a1, a2, b]); + expect(sorted.map((l) => l.key)).toEqual(["root", "b", "a", "a2", "a1"]); + }); +}); + +describe("isLayerDraggable", () => { + it("returns false for layers without id or selector", () => { + const layer = makeLayer({ key: "anon" }); + expect(isLayerDraggable(layer)).toBe(false); + }); + + it("returns true for layers with an id", () => { + const layer = makeLayer({ key: "with-id", id: "my-el" }); + expect(isLayerDraggable(layer)).toBe(true); + }); + + it("returns true for layers with a selector", () => { + const layer = makeLayer({ key: "with-sel", selector: ".my-class" }); + expect(isLayerDraggable(layer)).toBe(true); + }); + + it("returns false for layers inside a locked composition", () => { + const layer = makeLayer({ key: "locked", id: "locked-el", locked: true }); + expect(isLayerDraggable(layer)).toBe(false); + }); + + it("returns true for layers with id and no locked ancestor", () => { + const layer = makeLayer({ key: "free", id: "free-el" }); + expect(isLayerDraggable(layer)).toBe(true); + }); +}); diff --git a/packages/studio/src/components/editor/LayersPanel.tsx b/packages/studio/src/components/editor/LayersPanel.tsx index e18537f33..8dddc31f1 100644 --- a/packages/studio/src/components/editor/LayersPanel.tsx +++ b/packages/studio/src/components/editor/LayersPanel.tsx @@ -13,6 +13,7 @@ import { resolveTimelineSelectionSeekTime, } from "../../utils/studioHelpers"; import { Layers } from "../../icons/SystemIcons"; +import { useLayerDrag, isLayerDraggable, type LayerReorderEvent } from "./useLayerDrag"; const TAG_ICONS: Record = { video: "Vi", @@ -51,6 +52,7 @@ interface CollapsedState { [key: string]: boolean; } +// fallow-ignore-next-line complexity export const LayersPanel = memo(function LayersPanel() { const { previewIframeRef, @@ -59,12 +61,19 @@ export const LayersPanel = memo(function LayersPanel() { compositionLoading, timelineElements, currentTime, + showToast, } = useStudioContext(); - const { domEditSelection, applyDomSelection, updateDomEditHoverSelection } = useDomEditContext(); + const { + domEditSelection, + applyDomSelection, + updateDomEditHoverSelection, + handleDomZIndexReorderCommit, + } = useDomEditContext(); const [layers, setLayers] = useState([]); const [collapsed, setCollapsed] = useState({}); const prevDocVersionRef = useRef(0); + const scrollContainerRef = useRef(null); const isMasterView = !activeCompPath || activeCompPath === "index.html"; @@ -87,7 +96,7 @@ export const LayersPanel = memo(function LayersPanel() { activeCompositionPath: activeCompPath, isMasterView, }); - setLayers(items); + setLayers(sortLayersByZIndex(items)); }, [previewIframeRef, activeCompPath, isMasterView]); useEffect(() => { @@ -119,7 +128,6 @@ export const LayersPanel = memo(function LayersPanel() { isMasterView, preferClipAncestor: false, }), - // LayersPanel has no projectId; probe is skipped when projectId is absent [activeCompPath, isMasterView], ); @@ -130,8 +138,6 @@ export const LayersPanel = memo(function LayersPanel() { let matchedId = findMatchingTimelineElementId(selection, timelineElements); - // No direct match — walk up DOM ancestors to find the nearest element - // that has a timeline entry (e.g. a child of scene1 seeks to scene1.start) if (!matchedId) { const sourceFile = selection.sourceFile ?? "index.html"; let ancestor = layer.element.parentElement; @@ -185,10 +191,52 @@ export const LayersPanel = memo(function LayersPanel() { setCollapsed((prev) => ({ ...prev, [key]: !prev[key] })); }, []); - const selectedKey = domEditSelection ? getDomEditLayerKey(domEditSelection) : null; + const handleReorder = useCallback( + (event: LayerReorderEvent) => { + const { siblingLayers, fromIndex, toIndex } = event; + const reordered = [...siblingLayers]; + const [moved] = reordered.splice(fromIndex, 1); + reordered.splice(toIndex, 0, moved); + + const existingValues = siblingLayers.map((l) => getElementZIndex(l.element)); + const sorted = [...existingValues].sort((a, b) => b - a); + const hasDupes = sorted.some((v, i) => i > 0 && v === sorted[i - 1]); + const zValues = hasDupes ? reordered.map((_, i) => reordered.length - i) : sorted; + + const entries = reordered.map((layer, i) => ({ + element: layer.element, + zIndex: zValues[i], + id: layer.id, + selector: layer.selector, + selectorIndex: layer.selectorIndex, + sourceFile: layer.sourceFile, + })); + + handleDomZIndexReorderCommit(entries); + }, + [handleDomZIndexReorderCommit], + ); + const selectedKey = domEditSelection ? getDomEditLayerKey(domEditSelection) : null; const visibleLayers = getVisibleLayers(layers, collapsed); + const handleSingleSibling = useCallback(() => { + showToast("Only one layer at this level", "info"); + }, [showToast]); + + const { + dragKey, + insertionLineY, + handleRowPointerDown, + handleContainerPointerMove, + handleContainerPointerUp, + } = useLayerDrag({ + visibleLayers, + scrollContainerRef, + onReorder: handleReorder, + onSingleSibling: handleSingleSibling, + }); + if (layers.length === 0) { return (
@@ -207,9 +255,17 @@ export const LayersPanel = memo(function LayersPanel() {
{layers.length} layer{layers.length === 1 ? "" : "s"}
-
- {visibleLayers.map((layer) => { +
+ {visibleLayers.map((layer, index) => { const selected = layer.key === selectedKey; + const isDragged = layer.key === dragKey; + const draggable = isLayerDraggable(layer); const isCollapsed = collapsed[layer.key] ?? false; const hasChildren = layer.childCount > 0; const isCompHost = isCompositionHost(layer.element); @@ -217,21 +273,25 @@ export const LayersPanel = memo(function LayersPanel() { return (
handleSelectLayer(layer)} - onPointerEnter={() => handleLayerHover(layer)} + onClick={() => !dragKey && handleSelectLayer(layer)} + onPointerDown={(e) => handleRowPointerDown(index, e)} + onPointerEnter={() => !dragKey && handleLayerHover(layer)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleSelectLayer(layer); } }} - className={`group flex w-full cursor-pointer items-center gap-1.5 px-2 py-1 text-left transition-colors ${ - selected - ? "bg-studio-accent/14 text-studio-accent" - : "text-neutral-300 hover:bg-white/[0.04] hover:text-neutral-100" - }`} + className={`group flex w-full items-center gap-1.5 px-2 py-1 text-left transition-colors ${ + isDragged + ? "opacity-40" + : selected + ? "bg-studio-accent/14 text-studio-accent" + : "text-neutral-300 hover:bg-white/[0.04] hover:text-neutral-100" + } ${dragKey ? "cursor-grabbing" : draggable ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`} style={{ paddingLeft: 8 + layer.depth * 16 }} > {hasChildren ? ( @@ -271,11 +331,87 @@ export const LayersPanel = memo(function LayersPanel() {
); })} + {insertionLineY != null && ( +
+ )}
); }); +// ── Pure helpers ────────────────────────────────────────────────────── + +// fallow-ignore-next-line complexity +function getElementZIndex(element: HTMLElement): number { + try { + const inline = element.style?.zIndex; + if (inline && inline !== "auto") { + const parsed = parseInt(inline, 10); + if (Number.isFinite(parsed)) return parsed; + } + const win = element.ownerDocument?.defaultView; + if (!win) return 0; + const value = win.getComputedStyle(element).zIndex; + if (value === "auto" || value === "") return 0; + const parsed = parseInt(value, 10); + return Number.isFinite(parsed) ? parsed : 0; + } catch { + return 0; + } +} + +// fallow-ignore-next-line complexity +export function sortLayersByZIndex(layers: DomEditLayerItem[]): DomEditLayerItem[] { + if (layers.length <= 1) return layers; + + const minDepth = layers[0].depth; + for (let i = 1; i < layers.length; i++) { + if (layers[i].depth < minDepth) return layers; + } + + const chunks: Array<{ root: DomEditLayerItem; children: DomEditLayerItem[]; domIndex: number }> = + []; + + for (let i = 0; i < layers.length; i++) { + if (layers[i].depth === minDepth) { + const children: DomEditLayerItem[] = []; + let j = i + 1; + while (j < layers.length && layers[j].depth > minDepth) { + children.push(layers[j]); + j++; + } + chunks.push({ root: layers[i], children, domIndex: chunks.length }); + } + } + + if (chunks.length <= 1) { + if (chunks.length === 1 && chunks[0].children.length > 0) { + const sorted = sortLayersByZIndex(chunks[0].children); + return [chunks[0].root, ...sorted]; + } + return layers; + } + + chunks.sort((a, b) => { + const zA = getElementZIndex(a.root.element); + const zB = getElementZIndex(b.root.element); + if (zA !== zB) return zB - zA; + return b.domIndex - a.domIndex; + }); + + const result: DomEditLayerItem[] = []; + for (const chunk of chunks) { + result.push(chunk.root); + if (chunk.children.length > 0) { + result.push(...sortLayersByZIndex(chunk.children)); + } + } + return result; +} + function getVisibleLayers( layers: DomEditLayerItem[], collapsed: CollapsedState, diff --git a/packages/studio/src/components/editor/useLayerDrag.ts b/packages/studio/src/components/editor/useLayerDrag.ts new file mode 100644 index 000000000..ae8d75239 --- /dev/null +++ b/packages/studio/src/components/editor/useLayerDrag.ts @@ -0,0 +1,213 @@ +import { useRef, useState, useCallback } from "react"; +import type { DomEditLayerItem } from "./domEditingTypes"; + +const DRAG_THRESHOLD_PX = 4; + +interface DragState { + pointerId: number; + startY: number; + dragLayerIndex: number; + siblingIndices: number[]; + fromSiblingPos: number; + insertSiblingPos: number; + siblingRects: DOMRect[]; + activated: boolean; +} + +export interface LayerReorderEvent { + siblingLayers: DomEditLayerItem[]; + fromIndex: number; + toIndex: number; +} + +export interface UseLayerDragOptions { + visibleLayers: DomEditLayerItem[]; + scrollContainerRef: React.RefObject; + onReorder: (event: LayerReorderEvent) => void; + onSingleSibling?: () => void; +} + +export interface UseLayerDragReturn { + dragKey: string | null; + insertionLineY: number | null; + handleRowPointerDown: (layerIndex: number, e: React.PointerEvent) => void; + handleContainerPointerMove: (e: React.PointerEvent) => void; + handleContainerPointerUp: () => void; +} + +export function isLayerDraggable(layer: DomEditLayerItem): boolean { + if (!(layer.selector || layer.id)) return false; + let el: HTMLElement | null = layer.element; + while (el) { + if (el.hasAttribute("data-timeline-locked")) return false; + el = el.parentElement; + } + return true; +} + +function findSiblingIndices(visibleLayers: DomEditLayerItem[], layerIndex: number): number[] { + const depth = visibleLayers[layerIndex].depth; + const indices: number[] = []; + + let start = layerIndex; + while (start > 0) { + start--; + if (visibleLayers[start].depth < depth) { + start++; + break; + } + } + + for (let i = start; i < visibleLayers.length; i++) { + const d = visibleLayers[i].depth; + if (d < depth) break; + if (d === depth) indices.push(i); + } + + return indices; +} + +function measureSiblingRects(container: HTMLDivElement, siblingIndices: number[]): DOMRect[] { + const rows = container.querySelectorAll("[data-layer-index]"); + const rects: DOMRect[] = []; + for (const idx of siblingIndices) { + for (const row of rows) { + if (row.dataset.layerIndex === String(idx)) { + rects.push(row.getBoundingClientRect()); + break; + } + } + } + return rects; +} + +function computeInsertionPos(clientY: number, siblingRects: DOMRect[]): number { + if (siblingRects.length === 0) return 0; + + if (clientY <= siblingRects[0].top + siblingRects[0].height / 2) return 0; + + for (let i = 0; i < siblingRects.length - 1; i++) { + const midpoint = (siblingRects[i].bottom + siblingRects[i + 1].top) / 2; + if (clientY <= midpoint) return i + 1; + } + + const last = siblingRects[siblingRects.length - 1]; + if (clientY <= last.top + last.height / 2) return siblingRects.length - 1; + + return siblingRects.length; +} + +function computeInsertionLineY( + insertPos: number, + siblingRects: DOMRect[], + containerRect: DOMRect, +): number | null { + if (siblingRects.length === 0) return null; + if (insertPos <= 0) return siblingRects[0].top - containerRect.top; + if (insertPos >= siblingRects.length) { + return siblingRects[siblingRects.length - 1].bottom - containerRect.top; + } + return siblingRects[insertPos].top - containerRect.top; +} + +export function useLayerDrag({ + visibleLayers, + scrollContainerRef, + onReorder, + onSingleSibling, +}: UseLayerDragOptions): UseLayerDragReturn { + const dragRef = useRef(null); + const [dragKey, setDragKey] = useState(null); + const [insertionLineY, setInsertionLineY] = useState(null); + + const handleRowPointerDown = useCallback( + (layerIndex: number, e: React.PointerEvent) => { + if (e.button !== 0) return; + const layer = visibleLayers[layerIndex]; + if (!layer || !isLayerDraggable(layer)) return; + + const siblingIndices = findSiblingIndices(visibleLayers, layerIndex); + if (siblingIndices.length <= 1) { + onSingleSibling?.(); + return; + } + + const fromSiblingPos = siblingIndices.indexOf(layerIndex); + if (fromSiblingPos === -1) return; + + const container = scrollContainerRef.current; + if (!container) return; + + e.preventDefault(); + container.setPointerCapture(e.pointerId); + + dragRef.current = { + pointerId: e.pointerId, + startY: e.clientY, + dragLayerIndex: layerIndex, + siblingIndices, + fromSiblingPos, + insertSiblingPos: fromSiblingPos, + siblingRects: measureSiblingRects(container, siblingIndices), + activated: false, + }; + }, + [visibleLayers, scrollContainerRef, onSingleSibling], + ); + + const handleContainerPointerMove = useCallback( + (e: React.PointerEvent) => { + const drag = dragRef.current; + if (!drag) return; + + if (!drag.activated) { + if (Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD_PX) return; + drag.activated = true; + setDragKey(visibleLayers[drag.dragLayerIndex]?.key ?? null); + } + + const insertPos = computeInsertionPos(e.clientY, drag.siblingRects); + drag.insertSiblingPos = insertPos; + + const container = scrollContainerRef.current; + if (container) { + const containerRect = container.getBoundingClientRect(); + setInsertionLineY(computeInsertionLineY(insertPos, drag.siblingRects, containerRect)); + } + }, + [visibleLayers, scrollContainerRef], + ); + + const handleContainerPointerUp = useCallback(() => { + const drag = dragRef.current; + dragRef.current = null; + setDragKey(null); + setInsertionLineY(null); + + if (!drag || !drag.activated) return; + + const container = scrollContainerRef.current; + if (container) { + try { + container.releasePointerCapture(drag.pointerId); + } catch { + // already released + } + } + + let toPos = drag.insertSiblingPos; + if (toPos > drag.fromSiblingPos) toPos--; + if (toPos === drag.fromSiblingPos) return; + + const siblingLayers = drag.siblingIndices.map((i) => visibleLayers[i]); + onReorder({ siblingLayers, fromIndex: drag.fromSiblingPos, toIndex: toPos }); + }, [visibleLayers, scrollContainerRef, onReorder]); + + return { + dragKey, + insertionLineY, + handleRowPointerDown, + handleContainerPointerMove, + handleContainerPointerUp, + }; +} diff --git a/packages/studio/src/contexts/DomEditContext.tsx b/packages/studio/src/contexts/DomEditContext.tsx index 7fa2f1f8e..feed5118f 100644 --- a/packages/studio/src/contexts/DomEditContext.tsx +++ b/packages/studio/src/contexts/DomEditContext.tsx @@ -32,6 +32,7 @@ export function DomEditProvider({ handleDomHtmlAttributeCommit, handleDomPathOffsetCommit, handleDomGroupPathOffsetCommit, + handleDomZIndexReorderCommit, handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, @@ -100,6 +101,7 @@ export function DomEditProvider({ handleDomHtmlAttributeCommit, handleDomPathOffsetCommit, handleDomGroupPathOffsetCommit, + handleDomZIndexReorderCommit, handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, @@ -162,6 +164,7 @@ export function DomEditProvider({ handleDomHtmlAttributeCommit, handleDomPathOffsetCommit, handleDomGroupPathOffsetCommit, + handleDomZIndexReorderCommit, handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, diff --git a/packages/studio/src/hooks/useDomEditCommits.ts b/packages/studio/src/hooks/useDomEditCommits.ts index 0a995341f..c65e87b88 100644 --- a/packages/studio/src/hooks/useDomEditCommits.ts +++ b/packages/studio/src/hooks/useDomEditCommits.ts @@ -529,6 +529,54 @@ export function useDomEditCommits({ ], ); + const handleDomZIndexReorderCommit = useCallback( + ( + entries: Array<{ + element: HTMLElement; + zIndex: number; + id?: string; + selector?: string; + selectorIndex?: number; + sourceFile: string; + }>, + ) => { + if (entries.length === 0) return; + const coalesceKey = `z-reorder:${entries.map((e) => e.id ?? e.selector ?? "el").join(":")}`; + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + entry.element.style.zIndex = String(entry.zIndex); + const patches: Array<{ type: "inline-style"; property: string; value: string }> = [ + { type: "inline-style", property: "z-index", value: String(entry.zIndex) }, + ]; + try { + const win = entry.element.ownerDocument?.defaultView; + if (win && win.getComputedStyle(entry.element).position === "static") { + entry.element.style.position = "relative"; + patches.push({ type: "inline-style", property: "position", value: "relative" }); + } + } catch { + /* cross-origin or detached — skip */ + } + commitPositionPatchToHtml( + { + element: entry.element, + id: entry.id ?? null, + selector: entry.selector, + selectorIndex: entry.selectorIndex, + sourceFile: entry.sourceFile, + } as unknown as DomEditSelection, + patches, + { + label: "Reorder layers", + coalesceKey, + skipRefresh: i < entries.length - 1, + }, + ); + } + }, + [commitPositionPatchToHtml], + ); + return { resolveImportedFontAsset, handleDomStyleCommit, @@ -547,5 +595,6 @@ export function useDomEditCommits({ handleDomMotionCommit, handleDomMotionClear, handleDomEditElementDelete, + handleDomZIndexReorderCommit, }; } diff --git a/packages/studio/src/hooks/useDomEditSession.ts b/packages/studio/src/hooks/useDomEditSession.ts index 232b80d28..1ee449de8 100644 --- a/packages/studio/src/hooks/useDomEditSession.ts +++ b/packages/studio/src/hooks/useDomEditSession.ts @@ -289,6 +289,7 @@ export function useDomEditSession({ handleDomMotionCommit, handleDomMotionClear, handleDomEditElementDelete, + handleDomZIndexReorderCommit, } = useDomEditCommits({ activeCompPath, previewIframeRef, @@ -537,10 +538,8 @@ export function useDomEditSession({ agentModalAnchorPoint, copiedAgentPrompt, agentPromptSelectionContext, - // Refs domEditSelectionRef, - // Callbacks handleTimelineElementSelect, handlePreviewCanvasMouseDown, @@ -553,6 +552,7 @@ export function useDomEditSession({ handleDomHtmlAttributeCommit, handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit, handleDomGroupPathOffsetCommit, + handleDomZIndexReorderCommit, handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit, handleDomRotationCommit: handleGsapAwareRotationCommit, handleDomManualEditsReset,