From 26450c1a2776c463b514913ff17e205198ccc48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 19 May 2026 16:02:33 -0400 Subject: [PATCH] =?UTF-8?q?refactor:=20address=20review=20=E2=80=94=20rena?= =?UTF-8?q?me=20activateSiblingTimelines,=20opts=20arg,=20FIXME=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename activateNestedChildTimelines → activateSiblingTimelines (matches player.ts) - Use tl.play() instead of tl.paused(false) for consistency - Convert positional activateChildren boolean to { activateChildren } opts - Add FIXME(#969) to divergence test with tracking issue link - Add [id="intro"] no-rewrite boundary test - Add comment about deliberate no-restore behavior in render-seek path --- .../src/compiler/compositionScoping.test.ts | 11 +++++++ .../compiler/inlineSubCompositions.test.ts | 2 ++ packages/core/src/runtime/init.ts | 33 +++++++++---------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/core/src/compiler/compositionScoping.test.ts b/packages/core/src/compiler/compositionScoping.test.ts index 3af882f28..4362bb956 100644 --- a/packages/core/src/compiler/compositionScoping.test.ts +++ b/packages/core/src/compiler/compositionScoping.test.ts @@ -513,6 +513,17 @@ window.__afterTimeline = window.__timelines.scene; expect(scoped).not.toMatch(/#intro\b/); }); + it('does not rewrite [id="intro"] attribute selectors', () => { + // The function only targets #intro hash selectors, not [id="intro"] attribute selectors + const result = scopeCssToComposition( + '[id="intro"] .title { color: red; }', + "intro", + undefined, + "intro", + ); + expect(result).toContain('[id="intro"]'); + }); + it("wraps scripts with authored root id normalization for #id GSAP selectors", () => { const { document } = parseHTML(`
diff --git a/packages/core/src/compiler/inlineSubCompositions.test.ts b/packages/core/src/compiler/inlineSubCompositions.test.ts index 8edc15394..dd3e084ce 100644 --- a/packages/core/src/compiler/inlineSubCompositions.test.ts +++ b/packages/core/src/compiler/inlineSubCompositions.test.ts @@ -147,6 +147,8 @@ describe("inlineSubCompositions – #ID selector scoping divergence", () => { * Proper fix (follow-up): make the producer path add data-hf-authored-id * to the host element when the inner root has an id attribute. */ + // FIXME(#969): flip these assertions once the producer path adds + // data-hf-authored-id to the host element. See PR #965 "Proper fix (follow-up)". it("documents the divergence: producer path lacks data-hf-authored-id element", () => { const document = makeHostDocument("intro"); const host = document.querySelector('[data-composition-src="intro.html"]')!; diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 137fc3c4e..dc77e9429 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -1724,40 +1724,39 @@ export function initSandboxRuntimeModular(): void { } }; - // Unpause all non-root timelines. Per GSAP semantics, paused(false) on a - // child timeline that hasn't been reached by the parent's playhead is a - // no-op — the child won't fire onStart/onUpdate until the parent seeks - // past its insertion point. Per-frame visibility is gated by the engine. - const activateNestedChildTimelines = (masterTimeline: RuntimeTimelineLike) => { + // Unpause all non-root timelines registered in window.__timelines (siblings + // in the registry, not GSAP child tweens). Matches the naming convention in + // player.ts:32 (forEachSiblingTimeline) and player.ts:89 (activateSiblingTimelines). + // + // Unlike the player's seek path which re-pauses siblings after seeking, + // render-seek is one-frame-at-a-time with no transport tick between frames, + // so the residual unpaused state is harmless — the next call re-activates + // idempotently. + const activateSiblingTimelines = (masterTimeline: RuntimeTimelineLike) => { const timelines = (window.__timelines ?? {}) as Record; for (const tl of Object.values(timelines)) { if (!tl || tl === masterTimeline) continue; try { - const tlWithPaused = tl as RuntimeTimelineLike & { - paused?: (value?: boolean) => unknown; - }; - if (typeof tlWithPaused.paused === "function") { - tlWithPaused.paused(false); - } + tl.play(); } catch (err) { - swallow("runtime.init.activateNested", err); + swallow("runtime.init.activateSiblings", err); } } }; - const seekTimelineAndAdapters = (t: number, activateChildren = false) => { + const seekTimelineAndAdapters = (t: number, opts?: { activateChildren?: boolean }) => { const tl = state.capturedTimeline; if (tl) { // When rendering frame-by-frame (activateChildren=true), ensure all - // nested child timelines are unpaused before seeking the root. GSAP + // sibling timelines are unpaused before seeking the root. GSAP // does not propagate totalTime() to children that are internally // paused, which leaves sub-compositions at their initial CSS state // (typically opacity:0). This mirrors the activateSiblingTimelines // call in player.ts renderSeek and is critical for sub-compositions // whose data-start is at or near 0 — they are added to the root // while it is paused and may never receive an explicit play(). - if (activateChildren) { - activateNestedChildTimelines(tl); + if (opts?.activateChildren) { + activateSiblingTimelines(tl); } try { if (typeof tl.totalTime === "function") { @@ -2033,7 +2032,7 @@ export function initSandboxRuntimeModular(): void { state.currentTime = clock.now(); state.isPlaying = false; state.mediaForceSyncNextTick = true; - seekTimelineAndAdapters(state.currentTime, true); + seekTimelineAndAdapters(state.currentTime, { activateChildren: true }); syncMediaForCurrentState(); postState(true); };