diff --git a/packages/core/src/runtime/init.test.ts b/packages/core/src/runtime/init.test.ts index fed63cae3..ab054e557 100644 --- a/packages/core/src/runtime/init.test.ts +++ b/packages/core/src/runtime/init.test.ts @@ -165,6 +165,52 @@ describe("initSandboxRuntimeModular", () => { expect(child.style.visibility).toBe("hidden"); }); + it("keeps external composition hosts visible through their authored duration", async () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-root", "true"); + root.setAttribute("data-start", "0"); + root.setAttribute("data-width", "1920"); + root.setAttribute("data-height", "1080"); + document.body.appendChild(root); + + const child = document.createElement("div"); + child.setAttribute("data-composition-id", "sub"); + child.setAttribute("data-composition-src", "compositions/sub.html"); + child.setAttribute("data-start", "0"); + child.setAttribute("data-duration", "3"); + root.appendChild(child); + + const template = document.createElement("template"); + template.id = "sub-template"; + template.innerHTML = ` +
+
HOLD ME
+
+ `; + document.body.appendChild(template); + + (window as Window & { __timelines?: Record }).__timelines = { + main: createMockTimeline(3), + sub: createMockTimeline(1), + }; + + initSandboxRuntimeModular(); + await new Promise((resolve) => window.setTimeout(resolve, 0)); + + const player = ( + window as Window & { + __player?: { renderSeek: (timeSeconds: number) => void }; + } + ).__player; + expect(player).toBeDefined(); + expect(child.querySelector("#hold-marker")?.textContent).toBe("HOLD ME"); + + player?.renderSeek(2); + + expect(child.style.visibility).toBe("visible"); + }); + it("pads the root timeline to the authored composition schedule before seeking visibility", () => { const root = document.createElement("div"); root.setAttribute("data-composition-id", "main"); diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 7ecc5a594..04b9ca6ed 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -255,9 +255,9 @@ export function initSandboxRuntimeModular(): void { if (authoredEnd != null && !node.hasAttribute(AUTHORED_END_ATTR)) { node.setAttribute(AUTHORED_END_ATTR, authoredEnd); } - // Non-root compositions derive visible duration from timeline. - // Strip both data-duration AND data-end so the visibility system - // falls back to the GSAP timeline duration (parity with preview). + // Strip public timing attrs on non-root compositions after preserving + // authored values privately. Runtime timing can still distinguish + // authored host windows from live child timeline durations. node.removeAttribute("data-duration"); node.removeAttribute("data-end"); } @@ -1335,9 +1335,18 @@ export function initSandboxRuntimeModular(): void { } } - // Composition hosts must respect both the authored parent clip window - // and the child composition's own live timeline duration. - if (duration != null && duration > 0 && liveDuration != null) { + const usesExternalCompositionSlot = rawNode.hasAttribute("data-composition-src"); + + // Generic child compositions retain legacy behavior and respect both + // the authored parent clip window and the live child timeline duration. + // External composition hosts render into an authored slot, so a shorter + // child timeline should hold its final state through that slot. + if ( + duration != null && + duration > 0 && + liveDuration != null && + !usesExternalCompositionSlot + ) { duration = Math.min(duration, liveDuration); } else if ((duration == null || duration <= 0) && liveDuration != null) { duration = liveDuration;