fix(studio): stop injecting inline z-index on all clips during timeline edits (#959)

## Summary

- Remove the z-index injection loops from timeline move, delete, and asset-drop commit paths — only timing/track attributes are now patched on the affected clip, leaving all other clips untouched
- Fix `patchInlineStyleInTag` to handle self-closing void elements (`<img />`, `<audio />`) — the old code produced malformed `<img ... / style="z-index: 7">` output
- Delete the now-unused `buildTrackZIndexMap` helper and its tests

## Root cause

Three timeline operations (`handleTimelineElementMove`, `handleTimelineElementDelete`, `handleTimelineAssetDrop`) looped over every clip in the file on each commit and injected `style="z-index: N"` derived from an inverted `data-track-index` mapping via `buildTrackZIndexMap`. This overrode the author's CSS z-index — contradicting the documented contract that `data-track-index` does not affect visual layering — and persisted the corruption in the source HTML.

## Test plan

- [x] Reproduction test covering old bug behavior (inline z-index injection on all clips, inverted layering)
- [x] Verification tests confirming move/delete only patches the affected clip's timing attributes
- [x] Void element tests confirming `<img ... style="..." />` output (not `<img ... / style="...">`)
- [x] End-to-end browser test: opened Studio, dragged badge clip in timeline via CDP, verified only `data-start` changed on the dragged clip with zero inline z-index injections
- [x] Full test suite: 585 tests pass (54 files)
- [x] Build, lint, format, typecheck all green

Closes #958
This commit is contained in:
Miguel Ángel
2026-05-19 18:51:53 +02:00
committed by GitHub
8 changed files with 191 additions and 112 deletions
@@ -4,7 +4,6 @@ import {
buildPromptCopyText,
buildTimelineElementAgentPrompt,
buildTimelineAgentPrompt,
buildTrackZIndexMap,
canOffsetTrimClipStart,
getTimelineEditCapabilities,
hasPatchableTimelineTarget,
@@ -159,29 +158,6 @@ describe("resolveTimelineMove", () => {
});
});
describe("buildTrackZIndexMap", () => {
it("maps visually higher tracks onto higher z-index values", () => {
expect(buildTrackZIndexMap([-2, -1, 0, 3])).toEqual(
new Map([
[-2, 4],
[-1, 3],
[0, 2],
[3, 1],
]),
);
});
it("deduplicates tracks before assigning z-index values", () => {
expect(buildTrackZIndexMap([-1, 0, -1, 3, 3])).toEqual(
new Map([
[-1, 3],
[0, 2],
[3, 1],
]),
);
});
});
describe("canOffsetTrimClipStart", () => {
it("allows front trim for clips that carry playback offset metadata", () => {
expect(
@@ -114,12 +114,6 @@ export function resolveTimelineMove(
};
}
export function buildTrackZIndexMap(tracks: number[]): Map<number, number> {
const uniqueTracks = Array.from(new Set(tracks)).sort((a, b) => a - b);
const maxZIndex = uniqueTracks.length;
return new Map(uniqueTracks.map((track, index) => [track, maxZIndex - index]));
}
export function resolveTimelineResize(
input: TimelineResizeInput,
edge: "start" | "end",