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
@@ -263,6 +263,22 @@ describe("getTimelineEditCapabilities", () => {
});
});
it("locks all timeline edits for clips with data-timeline-locked", () => {
expect(
getTimelineEditCapabilities({
tag: "div",
duration: 8,
selector: '[data-composition-id="caption-highlight"]',
compositionSrc: "compositions/components/caption-highlight.html",
timelineLocked: true,
}),
).toEqual({
canMove: false,
canTrimStart: false,
canTrimEnd: false,
});
});
it("allows full editing of explicitly authored generic elements", () => {
expect(
getTimelineEditCapabilities({
@@ -211,8 +211,9 @@ export function getTimelineEditCapabilities(input: {
playbackStartAttr?: "media-start" | "playback-start";
sourceDuration?: number;
timingSource?: "authored" | "implicit";
timelineLocked?: boolean;
}): TimelineEditCapabilities {
if (input.timingSource === "implicit") {
if (input.timingSource === "implicit" || input.timelineLocked) {
return {
canMove: false,
canTrimStart: false,
@@ -280,6 +280,10 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
applyMediaMetadataFromElement(entry, el);
}
if (el.hasAttribute("data-timeline-locked")) {
entry.timelineLocked = true;
}
// Sub-compositions
const compSrc =
el.getAttribute("data-composition-src") || el.getAttribute("data-composition-file");
@@ -26,6 +26,8 @@ export interface TimelineElement {
compositionSrc?: string;
/** Whether this row came from authored clip timing or Studio's full-duration layer fallback. */
timingSource?: "authored" | "implicit";
/** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
timelineLocked?: boolean;
}
export type ZoomMode = "fit" | "manual";