diff --git a/packages/cli/src/commands/figma/component.test.ts b/packages/cli/src/commands/figma/component.test.ts index c44e246e9..0f8a7ad5a 100644 --- a/packages/cli/src/commands/figma/component.test.ts +++ b/packages/cli/src/commands/figma/component.test.ts @@ -74,7 +74,7 @@ describe("runComponentImport", () => { it("bakes literals and reports unresolved bindings when the index is empty", async () => { const { out, html } = await importHero(); - expect(html).toContain("background: #0066FF"); + expect(html).toContain("background-color: #0066FF"); expect(html).not.toContain("var("); expect(out.unresolved).toHaveLength(1); expect(out.unresolved[0]?.figmaId).toBe("VariableID:1:1"); diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index c82471986..b7c540f9d 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -950,7 +950,7 @@ export async function bundleToSingleHtml( document.body.appendChild(compScript); } - emitRootCompositionVariableStyles(document); + emitRootCompositionVariableStyles(document, compVariablesByComp); enforceCompositionPixelSizing(document); autoHealMissingCompositionIds(document); @@ -1014,29 +1014,114 @@ function compositionVariablesCssBlock( * every element declaring data-composition-variables gets a scoped stylesheet * rule so var(--slug, literal) references resolve during body parse. The * runtime injection remains define-if-absent, so it won't double-apply. + * + * `variablesByComp` (host-merged sub-composition values, keyed by runtime + * composition id) adds one rule per scope — the flattened inner root loses + * its data-composition-id, so the host selector is the only stable anchor. + * Exported for the producer's render compiler, which inlines sub-compositions + * through the shared module rather than this bundler. + * Returns whether a style element was appended. */ -function emitRootCompositionVariableStyles(document: Document): void { - const rules: string[] = []; - const htmlDeclared = readDeclaredDefaults(document.documentElement); - const htmlRule = compositionVariablesCssBlock(htmlDeclared, ":root"); - if (htmlRule) rules.push(htmlRule); - for (const el of [...document.querySelectorAll("[data-composition-variables]")]) { - const compId = el.getAttribute("data-composition-id"); - const elId = el.getAttribute("id"); - const selector = compId - ? cssAttributeSelector("data-composition-id", compId) - : elId - ? `#${elId}` - : null; - if (!selector) continue; - const rule = compositionVariablesCssBlock(readDeclaredDefaults(el), selector); - if (rule) rules.push(rule); - } - if (rules.length === 0) return; +export function emitRootCompositionVariableStyles( + document: Document, + variablesByComp: Record> = {}, + overrides: Record = {}, +): boolean { + const layerFor = makeVariableLayer(document, overrides); + const rules = [ + ...hostScopedVariableRules(variablesByComp, overrides), + ...rootDeclaredVariableRules(document, layerFor), + ...declarerVariableRules(document, layerFor), + ]; + if (rules.length === 0) return false; const style = document.createElement("style"); style.setAttribute("data-hf-composition-variables", ""); style.textContent = rules.join("\n\n"); document.head.appendChild(style); + return true; +} + +type VariableLayer = ( + declared: Record, + hostValues: Record, +) => Record; + +/** + * Layering for one declarer: authored stylesheet definitions win over + * declared defaults (the runtime's define-if-absent, applied statically) — + * a var already defined in any authored