Skip to content

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

Merged
miguel-heygen merged 1 commit into
mainfrom
worktree-fix+studio-timeline-zindex-injection
May 19, 2026
Merged

fix(studio): stop injecting inline z-index on all clips during timeline edits#959
miguel-heygen merged 1 commit into
mainfrom
worktree-fix+studio-timeline-zindex-injection

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

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

  • Reproduction test covering old bug behavior (inline z-index injection on all clips, inverted layering)
  • Verification tests confirming move/delete only patches the affected clip's timing attributes
  • Void element tests confirming <img ... style="..." /> output (not <img ... / style="...">)
  • 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
  • Full test suite: 585 tests pass (54 files)
  • Build, lint, format, typecheck all green

Closes #958

…ne edits

Timeline move, delete, and asset-drop operations were looping over every
clip in the file and writing style="z-index: N" derived from an inverted
data-track-index mapping. This silently 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.

Remove the z-index injection loops from all three timeline commit paths.
Move and delete now only patch timing/track attributes on the affected
clip. Asset drop still sets z-index on the newly created element via the
generated HTML, without touching existing clips. Delete the now-unused
buildTrackZIndexMap helper.

Also fix patchInlineStyleInTag to handle self-closing void elements: the
old code produced malformed `<img ... / style="z-index: 7">` because it
didn't account for the trailing `/` before appending the style attribute.

Closes #958
@github-actions
Copy link
Copy Markdown

Fallow audit report

Found 25 findings.

Duplication (14)
Severity Rule Location Description
minor fallow/code-duplication packages/studio/src/hooks/useDomEditCommits.ts:403 Code clone group 1 (11 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/useTimelineEditing.ts:90 Code clone group 2 (10 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/useTimelineEditing.ts:137 Code clone group 2 (10 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/useTimelineEditing.ts:231 Code clone group 1 (11 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.test.ts:369 Code clone group 3 (16 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.test.ts:423 Code clone group 3 (16 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.ts:157 Code clone group 4 (9 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.ts:321 Code clone group 5 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.ts:345 Code clone group 6 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.ts:347 Code clone group 4 (9 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.ts:356 Code clone group 5 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.ts:360 Code clone group 7 (9 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.ts:462 Code clone group 7 (9 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/sourcePatcher.ts:478 Code clone group 6 (8 lines, 2 instances)
Health (11)
Severity Rule Location Description
minor fallow/high-crap-score packages/studio/src/hooks/useTimelineEditing.ts:89 'handleTimelineElementMove' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
critical fallow/high-crap-score packages/studio/src/hooks/useTimelineEditing.ts:133 'handleTimelineElementResize' has CRAP score 272.0 (threshold: 30.0, cyclomatic 16)
major fallow/high-crap-score packages/studio/src/hooks/useTimelineEditing.ts:205 'handleTimelineElementDelete' has CRAP score 90.0 (threshold: 30.0, cyclomatic 9)
critical fallow/high-crap-score packages/studio/src/hooks/useTimelineEditing.ts:274 'handleTimelineAssetDrop' has CRAP score 110.0 (threshold: 30.0, cyclomatic 10)
major fallow/high-crap-score packages/studio/src/hooks/useTimelineEditing.ts:351 'handleTimelineFileDrop' has CRAP score 72.0 (threshold: 30.0, cyclomatic 8)
critical fallow/high-crap-score packages/studio/src/utils/blockInstaller.ts:39 'addBlockToProject' has CRAP score 462.0 (threshold: 30.0, cyclomatic 21)
major fallow/high-crap-score packages/studio/src/utils/sourcePatcher.ts:32 'splitInlineStyleDeclarations' has CRAP score 56.3 (threshold: 30.0, cyclomatic 14)
major fallow/high-crap-score packages/studio/src/utils/sourcePatcher.ts:104 'resolveSourceFile' has CRAP score 71.3 (threshold: 30.0, cyclomatic 16)
minor fallow/high-crap-score packages/studio/src/utils/sourcePatcher.ts:169 'patchInlineStyleInTag' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)
minor fallow/high-crap-score packages/studio/src/utils/sourcePatcher.ts:235 'findTagByTarget' has CRAP score 49.5 (threshold: 30.0, cyclomatic 13)
minor fallow/high-crap-score packages/studio/src/utils/sourcePatcher.ts:434 'patchHtmlAttributeInTag' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)

Generated by fallow.

// Add one
const newTag =
tag.replace(/>$/, "") + ` style="${prop}: ${escapeStyleAttributeValue(value, '"')}"`;
const selfClosing = /\s*\/$/.test(tag);
const newTag =
tag.replace(/>$/, "") + ` style="${prop}: ${escapeStyleAttributeValue(value, '"')}"`;
const selfClosing = /\s*\/$/.test(tag);
const base = selfClosing ? tag.replace(/\s*\/$/, "") : tag;
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid surgical fix. Root cause framing in the PR body matches the diff: three commit paths were injecting an inverted data-track-index -> z-index mapping onto every clip, overriding the documented "CSS is the layering source of truth" contract. Removing those loops in useTimelineEditing.ts and dropping buildTrackZIndexMap is the right call.

Calibrated strengths

  • sourcePatcher.ts:206-208 — the self-closing fix is correct under the existing findTagByTarget regex semantics. [^>]* captures up to but not including >, so for <img ... /> the captured tag ends in ... /. The old tag.replace(/>$/, "") was a no-op; the new selfClosing detect + ${base} ... " /" recipe restores <img ... style="..." /> cleanly, with replaceTagAtMatch re-prepending the > that's still in html.slice(match.end). Verified mentally for both branches (with-existing-style and without).
  • timelineZIndexInjection.test.ts reproduces the exact failure mode from issue #958 (inverted layering on the bg-video/title case) before asserting the fix. Good practice — the test would catch a regression if someone re-introduced the loop.
  • Test rename / placement: keeping sourcePatcher.test.ts for unit-level void-element coverage and adding a timelineZIndexInjection.test.ts for the behavioral reproduction is the right split.

Findings

  • importantCodeQL CI is failing on this PR with js/polynomial-redos on the new \s*\/$ regexes (sourcePatcher.ts:206-207). Practically a false positive: the input tag is bounded by the upstream [^>]* capture and the regex has a single linear \s* quantifier with no nested ambiguity, plus this is local-file edit code, not network-attacker input. But it's the new required-check failure introduced by this diff. Quickest path to green: rewrite without the regex, e.g.
    const trimmed = tag.trimEnd();
    const selfClosing = trimmed.endsWith("/");
    const base = selfClosing ? trimmed.slice(0, -1).trimEnd() : tag;
    Same behavior, no regex, CodeQL stops complaining. Worth doing before merge so the rule stays useful as a tripwire.
  • important — Backwards compat for projects that already have stale-injected inline z-index on disk from the prior bug. The fix prevents new injection but doesn't clean up existing artifacts; those projects will still render with inverted layering until someone manually deletes the inline styles. Not a blocker for this PR, but worth a follow-up: either a one-shot migration on project open, or a Studio toast/notice when inline z-index is detected on a .clip. Filing an issue is enough — does not need to land in this PR.
  • nituseTimelineEditing.ts:305 and blockInstaller.ts:127: Math.max(1, relevantElements.length + 1) is always >= 2. The clamp is a no-op; relevantElements.length + 1 is sufficient. Trivial.
  • nit — The new asset-drop z-index policy (new clip = length + 1, i.e. on top) is a reasonable default but is now implicit and undocumented. Worth a one-line comment in handleTimelineAssetDrop and addBlockToProject calling out "new clips get the highest current z-index; existing clips are not modified" so the next reader doesn't reverse-engineer it. Also consider whether CSS z-index author intent on the inserted asset (none yet, since it's brand-new) should be derived from a registered convention rather than a Studio default — but probably overkill for this PR.

CI status (verbatim)

  • Fallow audit | CI | FAILURE — body shows only pre-existing CRAP-score + duplication minor findings in useTimelineEditing.ts / sourcePatcher.ts; nothing new introduced by this diff (the diff in fact reduces code in the flagged functions). Non-blocking.
  • CodeQL | (top-level) | FAILUREjs/polynomial-redos x2 on sourcePatcher.ts:206-207. Discussed above.
  • Render on windows-latest | Windows render verification | IN_PROGRESS — pending, not decisive.
  • All other required checks (Build, Test, Typecheck, Lint, Format, CLI smoke, regression, preview-regression, player-perf) are SUCCESS at head 4916d658.

Verdict: APPROVE
Reasoning: Bug fix is correct, well-tested, and the diff is appropriately scoped. The CodeQL failure is the only real concern — a 4-line rewrite to drop the regex keeps the static-analysis tripwire honest and clears the required check. Backwards-compat note is a follow-up, not a blocker.

Review by Vai

@miguel-heygen miguel-heygen merged commit 5939226 into main May 19, 2026
30 of 32 checks passed
Copy link
Copy Markdown
Collaborator Author

Merge activity

@miguel-heygen miguel-heygen deleted the worktree-fix+studio-timeline-zindex-injection branch May 19, 2026 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(studio) timeline drag rewrites HTML with inverted inline z-index on every clip, overrides CSS z-index documented as layering source of truth

3 participants