mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint
Brand-loop live test (SDS duplicate, plans/figma/brand-loop-test-plan.md) proved the recolor chain end-to-end and surfaced three gaps: - runtime now defines every declared composition variable as a CSS custom property (document root at init + scoped sub-comp hosts in the loader), so imported var(--slug, literal) fills resolve live — without this the frozen literal always won and variable-driven rebranding could not propagate. Slug kept byte-compatible with the figma importer (parity test). render --variables overrides win. - figma component --name: variant frames are often all named 'Platform=Desktop' and slug-collided across imports. - imported fragments carry data-hf-snippet and the project linter skips composition-root rules for them. - /figma skill documents the field-tested non-Enterprise tokens path (MCP get_variable_defs joined with REST boundVariables ids). Shared-helper extractions (injectScopedStyles, flattenedRoot module, parseHostVariableValues, rasterizeFallback, shapeCss) satisfy the dedup/complexity audit the runtime changes tripped. Validated live: brand-loop renders purple from the attribute alone (no manual :root); 118 figma + 662 runtime/compiler + 331 lint tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
80271863d3
commit
def276524b
@@ -1,5 +1,12 @@
|
||||
import { scopeCssToComposition, wrapScopedCompositionScript } from "../compiler/compositionScoping";
|
||||
import { readDeclaredDefaults } from "./getVariables";
|
||||
import { markFlattenedInnerRoot } from "./flattenedRoot";
|
||||
import {
|
||||
applyCssVariables,
|
||||
clearAppliedCssVariables,
|
||||
parseHostVariableValues,
|
||||
readDeclaredDefaults,
|
||||
readRenderOverrides,
|
||||
} from "./getVariables";
|
||||
|
||||
type LoadExternalCompositionsParams = {
|
||||
injectedStyles: HTMLStyleElement[];
|
||||
@@ -172,30 +179,9 @@ function resetCompositionHost(host: Element) {
|
||||
host.textContent = "";
|
||||
}
|
||||
|
||||
const FLATTENED_INNER_ROOT_STRIP_ATTRS = [
|
||||
"data-composition-id",
|
||||
"data-composition-file",
|
||||
"data-start",
|
||||
"data-duration",
|
||||
"data-end",
|
||||
"data-track-index",
|
||||
"data-track",
|
||||
"data-composition-src",
|
||||
"data-hf-authored-duration",
|
||||
"data-hf-authored-end",
|
||||
];
|
||||
|
||||
function prepareFlattenedInnerRoot(innerRoot: HTMLElement): HTMLElement {
|
||||
const prepared = document.importNode(innerRoot, true) as HTMLElement;
|
||||
const authoredRootId = prepared.getAttribute("id")?.trim();
|
||||
for (const attrName of FLATTENED_INNER_ROOT_STRIP_ATTRS) {
|
||||
prepared.removeAttribute(attrName);
|
||||
}
|
||||
if (authoredRootId) {
|
||||
prepared.removeAttribute("id");
|
||||
prepared.setAttribute("data-hf-authored-id", authoredRootId);
|
||||
}
|
||||
prepared.setAttribute("data-hf-inner-root", "true");
|
||||
markFlattenedInnerRoot(prepared);
|
||||
const w = prepared.getAttribute("data-width");
|
||||
const h = prepared.getAttribute("data-height");
|
||||
prepared.style.width = w ? `${w}px` : "100%";
|
||||
@@ -220,19 +206,6 @@ function resolveScriptSourceUrl(scriptSrc: string, compositionUrl: URL | null):
|
||||
}
|
||||
}
|
||||
|
||||
function parseHostVariableValues(host: Element): Record<string, unknown> {
|
||||
const raw = host.getAttribute("data-variable-values");
|
||||
if (!raw) return {};
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
type HostCompositionIdentity = {
|
||||
authoredCompositionId: string | null;
|
||||
runtimeCompositionId: string | null;
|
||||
@@ -423,10 +396,8 @@ async function mountCompositionContent(params: {
|
||||
}
|
||||
}
|
||||
|
||||
// Inject <head> styles from non-template sub-compositions first (they define
|
||||
// element styles like backgrounds and positioning that the composition needs).
|
||||
if (params.headStyles) {
|
||||
for (const style of params.headStyles) {
|
||||
const injectScopedStyles = (styleEls: Iterable<HTMLStyleElement>): void => {
|
||||
for (const style of styleEls) {
|
||||
const clonedStyle = style.cloneNode(true);
|
||||
if (!(clonedStyle instanceof HTMLStyleElement)) continue;
|
||||
if (authoredScopeCompositionId) {
|
||||
@@ -440,23 +411,12 @@ async function mountCompositionContent(params: {
|
||||
document.head.appendChild(clonedStyle);
|
||||
params.injectedStyles.push(clonedStyle);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = Array.from(contentNode.querySelectorAll<HTMLStyleElement>("style"));
|
||||
for (const style of styles) {
|
||||
const clonedStyle = style.cloneNode(true);
|
||||
if (!(clonedStyle instanceof HTMLStyleElement)) continue;
|
||||
if (authoredScopeCompositionId) {
|
||||
clonedStyle.textContent = scopeCssToComposition(
|
||||
clonedStyle.textContent || "",
|
||||
authoredScopeCompositionId,
|
||||
runtimeScopeSelector,
|
||||
authoredRootId,
|
||||
);
|
||||
}
|
||||
document.head.appendChild(clonedStyle);
|
||||
params.injectedStyles.push(clonedStyle);
|
||||
}
|
||||
};
|
||||
// Inject <head> styles from non-template sub-compositions first (they define
|
||||
// element styles like backgrounds and positioning that the composition needs),
|
||||
// then the content styles.
|
||||
if (params.headStyles) injectScopedStyles(params.headStyles);
|
||||
injectScopedStyles(Array.from(contentNode.querySelectorAll<HTMLStyleElement>("style")));
|
||||
|
||||
// Collect head scripts first (e.g. GSAP CDN loaded in <head> of non-template sub-comps),
|
||||
// then content scripts. Head scripts must execute before content scripts.
|
||||
@@ -536,16 +496,7 @@ async function mountCompositionContent(params: {
|
||||
// `window.__hfVariablesByComp[compId]`, so this table must be populated
|
||||
// before the wrapped IIFE evaluates.
|
||||
if (runtimeScopeCompositionId) {
|
||||
const merged = {
|
||||
...(params.declaredVariableDefaults ?? {}),
|
||||
...parseHostVariableValues(params.host),
|
||||
};
|
||||
if (Object.keys(merged).length > 0) {
|
||||
if (!window.__hfVariablesByComp) window.__hfVariablesByComp = {};
|
||||
window.__hfVariablesByComp[runtimeScopeCompositionId] = merged;
|
||||
} else if (window.__hfVariablesByComp) {
|
||||
delete window.__hfVariablesByComp[runtimeScopeCompositionId];
|
||||
}
|
||||
stashInstanceVariables(params, contentNode, runtimeScopeCompositionId);
|
||||
}
|
||||
|
||||
for (const scriptPayload of scriptPayloads) {
|
||||
@@ -761,3 +712,35 @@ export async function loadExternalCompositions(
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stash per-instance variables BEFORE running scripts (the scoped
|
||||
* getVariables() reads window.__hfVariablesByComp[compId]) and mirror them
|
||||
* as CSS custom properties on the host so imported var(--slug, literal)
|
||||
* fills inside the sub-comp resolve per instance (cascade beats the document
|
||||
* root). Inline templates carry declared defaults on the content root;
|
||||
* external loads pass them explicitly. Render-time overrides (--variables)
|
||||
* always win. Stale custom properties from a previous mount are cleared
|
||||
* before (re)applying.
|
||||
*/
|
||||
function stashInstanceVariables(
|
||||
params: { host: Element; declaredVariableDefaults?: Record<string, unknown> },
|
||||
contentNode: Node,
|
||||
runtimeScopeCompositionId: string,
|
||||
): void {
|
||||
const declaredDefaults =
|
||||
params.declaredVariableDefaults ??
|
||||
(contentNode instanceof Element ? readDeclaredDefaults(contentNode) : {});
|
||||
const merged = {
|
||||
...declaredDefaults,
|
||||
...parseHostVariableValues(params.host),
|
||||
};
|
||||
clearAppliedCssVariables(params.host);
|
||||
if (Object.keys(merged).length > 0) {
|
||||
if (!window.__hfVariablesByComp) window.__hfVariablesByComp = {};
|
||||
window.__hfVariablesByComp[runtimeScopeCompositionId] = merged;
|
||||
applyCssVariables(params.host, { ...merged, ...readRenderOverrides() });
|
||||
} else if (window.__hfVariablesByComp) {
|
||||
delete window.__hfVariablesByComp[runtimeScopeCompositionId];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Shared between the build-time bundler (htmlBundler) and the runtime
|
||||
* composition loader: when a sub-composition's inner root is flattened into
|
||||
* its host, these timing/identity attributes must be stripped and the
|
||||
* authored id preserved as data — identical semantics in both worlds.
|
||||
*/
|
||||
|
||||
export const FLATTENED_INNER_ROOT_STRIP_ATTRS = [
|
||||
"data-composition-id",
|
||||
"data-composition-file",
|
||||
"data-start",
|
||||
"data-duration",
|
||||
"data-end",
|
||||
"data-track-index",
|
||||
"data-track",
|
||||
"data-composition-src",
|
||||
"data-hf-authored-duration",
|
||||
"data-hf-authored-end",
|
||||
];
|
||||
|
||||
/** Strip timing attrs, demote the authored id, and mark the flattened root. */
|
||||
export function markFlattenedInnerRoot(prepared: Element): void {
|
||||
const authoredRootId = prepared.getAttribute("id")?.trim();
|
||||
for (const attrName of FLATTENED_INNER_ROOT_STRIP_ATTRS) {
|
||||
prepared.removeAttribute(attrName);
|
||||
}
|
||||
if (authoredRootId) {
|
||||
prepared.removeAttribute("id");
|
||||
prepared.setAttribute("data-hf-authored-id", authoredRootId);
|
||||
}
|
||||
prepared.setAttribute("data-hf-inner-root", "true");
|
||||
}
|
||||
@@ -203,3 +203,96 @@ describe("readDeclaredDefaults", () => {
|
||||
expect(readDeclaredDefaults(b)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("css variable injection (figma brand-token chain)", () => {
|
||||
afterEach(() => {
|
||||
document.documentElement.removeAttribute(VARIABLES_ATTR);
|
||||
document.documentElement.removeAttribute("data-hf-css-vars");
|
||||
document.documentElement.style.cssText = "";
|
||||
document.body.innerHTML = "";
|
||||
delete (window as Window & { __hfVariables?: unknown }).__hfVariables;
|
||||
});
|
||||
|
||||
it("slug stays byte-compatible with the figma importer", async () => {
|
||||
const { slugify } = await import("../figma/nodeToHtml");
|
||||
const { cssVariableName } = await import("../tokenSlug");
|
||||
for (const id of [
|
||||
"figma:sds-color-background-brand-default",
|
||||
"figma:Brand/Primary 500",
|
||||
"figma:Acme/Semantic/Blue-500",
|
||||
"!!!",
|
||||
]) {
|
||||
expect(cssVariableName(id)).toBe(`--${slugify(id)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("defines declared variables on the DECLARING element, not globally", async () => {
|
||||
const { injectCompositionCssVariables } = await import("./getVariables");
|
||||
document.body.innerHTML = `<div id="root" ${VARIABLES_ATTR}='[{"id":"figma:brand/primary","type":"color","label":"p","default":"#112233"}]'></div>`;
|
||||
injectCompositionCssVariables(document);
|
||||
const root = document.getElementById("root") as HTMLElement;
|
||||
expect(root.style.getPropertyValue("--figma-brand-primary")).toBe("#112233");
|
||||
expect(document.documentElement.style.getPropertyValue("--figma-brand-primary")).toBe("");
|
||||
});
|
||||
|
||||
it("two compositions on one page keep their own same-id values", async () => {
|
||||
const { injectCompositionCssVariables } = await import("./getVariables");
|
||||
document.body.innerHTML =
|
||||
`<div id="a" ${VARIABLES_ATTR}='[{"id":"figma:brand","type":"color","label":"b","default":"#aa0000"}]'></div>` +
|
||||
`<div id="b" ${VARIABLES_ATTR}='[{"id":"figma:brand","type":"color","label":"b","default":"#0000bb"}]'></div>`;
|
||||
injectCompositionCssVariables(document);
|
||||
expect(
|
||||
(document.getElementById("a") as HTMLElement).style.getPropertyValue("--figma-brand"),
|
||||
).toBe("#aa0000");
|
||||
expect(
|
||||
(document.getElementById("b") as HTMLElement).style.getPropertyValue("--figma-brand"),
|
||||
).toBe("#0000bb");
|
||||
});
|
||||
|
||||
it("declared defaults do NOT clobber an authored inline definition (define-if-absent)", async () => {
|
||||
const { injectCompositionCssVariables } = await import("./getVariables");
|
||||
document.body.innerHTML = `<div id="root" style="--accent: #3b82f6" ${VARIABLES_ATTR}='[{"id":"accent","type":"color","label":"a","default":"#ff5722"}]'></div>`;
|
||||
injectCompositionCssVariables(document);
|
||||
expect(
|
||||
(document.getElementById("root") as HTMLElement).style.getPropertyValue("--accent"),
|
||||
).toBe("#3b82f6");
|
||||
});
|
||||
|
||||
it("render-time overrides win over declared defaults AND authored values", async () => {
|
||||
const { injectCompositionCssVariables } = await import("./getVariables");
|
||||
document.body.innerHTML = `<div id="root" style="--figma-brand: #000000" ${VARIABLES_ATTR}='[{"id":"figma:brand","type":"color","label":"b","default":"#2c2c2c"}]'></div>`;
|
||||
(window as Window & { __hfVariables?: Record<string, unknown> }).__hfVariables = {
|
||||
"figma:brand": "#00ff99",
|
||||
};
|
||||
injectCompositionCssVariables(document);
|
||||
expect(
|
||||
(document.getElementById("root") as HTMLElement).style.getPropertyValue("--figma-brand"),
|
||||
).toBe("#00ff99");
|
||||
});
|
||||
|
||||
it("skips empty-string values (setProperty would remove, not define)", async () => {
|
||||
const { applyCssVariables } = await import("./getVariables");
|
||||
const el = document.createElement("div");
|
||||
applyCssVariables(el, { blank: "", real: "#123456" });
|
||||
expect(el.style.getPropertyValue("--blank")).toBe("");
|
||||
expect(el.style.getPropertyValue("--real")).toBe("#123456");
|
||||
expect(el.getAttribute("data-hf-css-vars")).toBe("--real");
|
||||
});
|
||||
|
||||
it("clearAppliedCssVariables removes exactly what was applied", async () => {
|
||||
const { applyCssVariables, clearAppliedCssVariables } = await import("./getVariables");
|
||||
const el = document.createElement("div");
|
||||
el.style.setProperty("--authored", "keep");
|
||||
applyCssVariables(el, { "figma:brand": "#111111" });
|
||||
clearAppliedCssVariables(el);
|
||||
expect(el.style.getPropertyValue("--figma-brand")).toBe("");
|
||||
expect(el.style.getPropertyValue("--authored")).toBe("keep");
|
||||
expect(el.hasAttribute("data-hf-css-vars")).toBe(false);
|
||||
});
|
||||
|
||||
it("getVariables() honors element-declared variables like the injection does", async () => {
|
||||
const { getVariables } = await import("./getVariables");
|
||||
document.body.innerHTML = `<div ${VARIABLES_ATTR}='[{"id":"figma:brand","type":"color","label":"b","default":"#445566"}]'></div>`;
|
||||
expect(getVariables()["figma:brand"]).toBe("#445566");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,13 @@ export function getVariables<
|
||||
>(): Partial<T> {
|
||||
if (typeof document === "undefined") return {} as Partial<T>;
|
||||
|
||||
// Same collection the CSS-variable injection uses: <html> first, then any
|
||||
// composition element carrying the attribute (later declarers win), then
|
||||
// render-time overrides.
|
||||
const declaredDefaults = readDeclaredDefaults(document.documentElement);
|
||||
for (const el of Array.from(document.querySelectorAll("[data-composition-variables]"))) {
|
||||
Object.assign(declaredDefaults, readDeclaredDefaults(el));
|
||||
}
|
||||
const overrides = readOverrides();
|
||||
|
||||
return { ...declaredDefaults, ...overrides } as Partial<T>;
|
||||
@@ -59,6 +65,125 @@ export function readDeclaredDefaults(root: Element | null): Record<string, unkno
|
||||
return out;
|
||||
}
|
||||
|
||||
import { cssVariableName, detectSlugCollisions } from "../tokenSlug";
|
||||
|
||||
const APPLIED_VARS_ATTR = "data-hf-css-vars";
|
||||
|
||||
function hasInlineStyle(target: Element): target is Element & ElementCSSInlineStyle {
|
||||
return "style" in target && typeof (target as HTMLElement).style?.setProperty === "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* Define primitive-valued variables as CSS custom properties on `target`,
|
||||
* recording the applied names so re-init/unmount can clear them. Empty
|
||||
* strings are skipped — CSSOM setProperty("", …) REMOVES the property, and a
|
||||
* blank default has no CSS meaning anyway.
|
||||
*/
|
||||
export function applyCssVariables(target: Element, variables: Record<string, unknown>): void {
|
||||
if (!hasInlineStyle(target)) return;
|
||||
const applied: string[] = [];
|
||||
for (const [id, value] of Object.entries(variables)) {
|
||||
if ((typeof value === "string" && value !== "") || typeof value === "number") {
|
||||
const name = cssVariableName(id);
|
||||
target.style.setProperty(name, String(value));
|
||||
applied.push(name);
|
||||
}
|
||||
}
|
||||
if (applied.length > 0) target.setAttribute(APPLIED_VARS_ATTR, applied.join(" "));
|
||||
}
|
||||
|
||||
/** Remove custom properties a previous applyCssVariables call defined. */
|
||||
export function clearAppliedCssVariables(target: Element): void {
|
||||
if (!hasInlineStyle(target)) return;
|
||||
const applied = target.getAttribute(APPLIED_VARS_ATTR);
|
||||
if (!applied) return;
|
||||
for (const name of applied.split(" ")) {
|
||||
if (name.startsWith("--")) target.style.removeProperty(name);
|
||||
}
|
||||
target.removeAttribute(APPLIED_VARS_ATTR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imported figma components reference brand tokens as `var(--slug, literal)`.
|
||||
* Define each declaring element's composition variables as CSS custom
|
||||
* properties ON THAT ELEMENT — scoping by the cascade, so two compositions
|
||||
* on one page can't clobber each other and a flattened sub-composition root
|
||||
* correctly styles only its own subtree.
|
||||
*
|
||||
* Declared DEFAULTS are define-if-absent: a value the author already styled
|
||||
* (stylesheet `:root` rule, compile-time emission, hand-written inline) wins
|
||||
* over the declared default, preserving pre-existing conventions where a
|
||||
* variable id coincides with an authored custom property. Render-time
|
||||
* overrides (`--variables` → `window.__hfVariables`) always win — that's
|
||||
* explicit user intent.
|
||||
*/
|
||||
export function injectCompositionCssVariables(doc: Document): void {
|
||||
const declarers = new Set<Element>();
|
||||
if (doc.documentElement?.hasAttribute("data-composition-variables")) {
|
||||
declarers.add(doc.documentElement);
|
||||
}
|
||||
for (const el of Array.from(doc.querySelectorAll("[data-composition-variables]"))) {
|
||||
declarers.add(el);
|
||||
}
|
||||
const overrides = readOverrides();
|
||||
const allIds: string[] = [];
|
||||
for (const el of declarers) {
|
||||
allIds.push(...applyDeclaredForElement(el, overrides, doc.defaultView));
|
||||
}
|
||||
for (const group of detectSlugCollisions(allIds)) {
|
||||
console.warn(
|
||||
`composition variables ${group.join(", ")} collapse to the same CSS property ${cssVariableName(group[0] ?? "")} — rename one to avoid cross-talk`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply one declarer's variables (define-if-absent for defaults, overrides
|
||||
* always win). Returns the declared ids for collision reporting. */
|
||||
function applyDeclaredForElement(
|
||||
el: Element,
|
||||
overrides: Record<string, unknown>,
|
||||
view: Window | null,
|
||||
): string[] {
|
||||
if (!hasInlineStyle(el)) return [];
|
||||
const declared = readDeclaredDefaults(el);
|
||||
const toApply: Record<string, unknown> = {};
|
||||
for (const [id, value] of Object.entries(declared)) {
|
||||
if (id in overrides) continue; // overrides applied below, always win
|
||||
const name = cssVariableName(id);
|
||||
// define-if-absent: respect authored/compiled definitions
|
||||
const existing =
|
||||
el.style.getPropertyValue(name) ||
|
||||
(view ? view.getComputedStyle(el).getPropertyValue(name) : "");
|
||||
if (existing.trim() !== "") continue;
|
||||
toApply[id] = value;
|
||||
}
|
||||
for (const [id, value] of Object.entries(overrides)) {
|
||||
if (id in declared) toApply[id] = value;
|
||||
}
|
||||
applyCssVariables(el, toApply);
|
||||
return Object.keys(declared);
|
||||
}
|
||||
|
||||
/** Parse a host element's `data-variable-values` JSON attribute (per-instance
|
||||
* sub-composition overrides). Shared by the runtime loader and the bundler. */
|
||||
export function parseHostVariableValues(host: Element): Record<string, unknown> {
|
||||
const raw = host.getAttribute("data-variable-values");
|
||||
if (!raw) return {};
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Render-time variable overrides (`hyperframes render --variables`). */
|
||||
export function readRenderOverrides(): Record<string, unknown> {
|
||||
return readOverrides();
|
||||
}
|
||||
|
||||
function readOverrides(): Record<string, unknown> {
|
||||
if (typeof window === "undefined") return {};
|
||||
const raw = (window as Window & { __hfVariables?: unknown }).__hfVariables;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// fallow-ignore-file code-duplication complexity
|
||||
import { installRuntimeControlBridge, postRuntimeMessage } from "./bridge";
|
||||
import { initRuntimeAnalytics, emitAnalyticsEvent } from "./analytics";
|
||||
import { injectCompositionCssVariables } from "./getVariables";
|
||||
import { createCssAdapter } from "./adapters/css";
|
||||
import { createGsapAdapter } from "./adapters/gsap";
|
||||
import { createAnimeJsAdapter } from "./adapters/animejs";
|
||||
@@ -141,6 +142,15 @@ export function initSandboxRuntimeModular(): void {
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
// figma brand-token chain: define declared composition variables as CSS
|
||||
// custom properties so imported var(--slug, literal) fills resolve from the
|
||||
// live variable instead of always falling back to the frozen literal.
|
||||
try {
|
||||
injectCompositionCssVariables(document);
|
||||
} catch (err) {
|
||||
swallow("runtime.init.cssVariables", err);
|
||||
}
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
// Resolve the root composition element with the same priority the rest of
|
||||
|
||||
Reference in New Issue
Block a user