feat: data-timeline-locked + fix font loss in sub-compositions

Timeline locking:
- Add data-timeline-locked attribute support — fully disables move,
  trim-start, and trim-end in Studio for clips that carry this attr
- Runtime propagates the attribute from inner composition root to host
  element so component authors control it from their HTML
- All 15 caption components in the registry now carry the attribute

Font fix:
- Extract <link rel="stylesheet"> and <link rel="preconnect"> from
  sub-composition <head> alongside existing <style>/<script> extraction
- Fixes caption components (and any sub-comp using Google Fonts via
  <link> tags) losing their font-family when loaded as sub-compositions
- Applied in both runtime (compositionLoader) and compiler
  (inlineSubCompositions) paths
This commit is contained in:
Miguel Ángel
2026-05-20 01:55:17 -04:00
parent 6d9670fd58
commit 471b4efb4b
23 changed files with 98 additions and 17 deletions
@@ -262,6 +262,8 @@ async function mountCompositionContent(params: {
headStyles?: HTMLStyleElement[];
/** Extra <script> elements from the parsed document <head> (non-template sub-compositions). */
headScripts?: HTMLScriptElement[];
/** Extra <link> elements from the parsed document <head> (font stylesheets, preconnects). */
headLinks?: HTMLLinkElement[];
/**
* Defaults extracted from the sub-composition's own
* `<html data-composition-variables="...">` attribute. Layered under the
@@ -297,6 +299,15 @@ async function mountCompositionContent(params: {
? `[data-composition-id="${CSS.escape(runtimeScopeCompositionId)}"]`
: undefined;
if (params.headLinks) {
for (const link of params.headLinks) {
const href = link.getAttribute("href") || "";
if (!href) continue;
if (document.head.querySelector(`link[href="${CSS.escape(href)}"]`)) continue;
document.head.appendChild(link.cloneNode(true));
}
}
// Inject <head> styles from non-template sub-compositions first (they define
// element styles like backgrounds and positioning that the composition needs).
if (params.headStyles) {
@@ -395,6 +406,9 @@ async function mountCompositionContent(params: {
if (heightRaw) params.host.setAttribute("data-height", heightRaw);
if (widthPx && params.host instanceof HTMLElement) params.host.style.width = widthPx;
if (heightPx && params.host instanceof HTMLElement) params.host.style.height = heightPx;
if (innerRoot.hasAttribute("data-timeline-locked")) {
params.host.setAttribute("data-timeline-locked", "");
}
params.host.appendChild(prepareFlattenedInnerRoot(innerRoot));
} else if (params.hasTemplate) {
params.host.appendChild(document.importNode(contentNode, true));
@@ -581,6 +595,13 @@ export async function loadExternalCompositions(
const headScripts = !template
? Array.from(doc.head.querySelectorAll<HTMLScriptElement>("script"))
: undefined;
const headLinks = !template
? Array.from(
doc.head.querySelectorAll<HTMLLinkElement>(
'link[rel="stylesheet"], link[rel="preconnect"]',
),
)
: undefined;
await mountCompositionContent({
host,
authoredCompositionId,
@@ -595,6 +616,7 @@ export async function loadExternalCompositions(
parseDimensionPx: params.parseDimensionPx,
headStyles,
headScripts,
headLinks,
declaredVariableDefaults: readDeclaredDefaults(doc.documentElement),
onDiagnostic: params.onDiagnostic,
});