diff --git a/packages/core/src/figma/nodeToHtml.test.ts b/packages/core/src/figma/nodeToHtml.test.ts index 7f70a61dc..367311474 100644 --- a/packages/core/src/figma/nodeToHtml.test.ts +++ b/packages/core/src/figma/nodeToHtml.test.ts @@ -84,6 +84,24 @@ describe("nodeToHtml", () => { expect(out.html).not.toContain("left: 520px"); }); + it("prefixes digit-leading slugs so ids stay CSS-selector-safe", () => { + const out = nodeToHtml( + frame([ + { + id: "1:2", + name: "3D Object - Headphones", + type: "RECTANGLE", + absoluteBoundingBox: BOX(120, 220, 100, 40), + fills: [SOLID_BLUE], + }, + ]), + { resolved: [], unresolved: [] }, + ); + // "#3d-object-headphones" would throw in querySelector/GSAP targeting + expect(out.html).toContain('id="n3d-object-headphones"'); + expect(out.html).not.toContain('id="3d-object-headphones"'); + }); + it("emits var() with literal fallback for resolved bindings", () => { const out = nodeToHtml( frame([ diff --git a/packages/core/src/figma/nodeToHtml.ts b/packages/core/src/figma/nodeToHtml.ts index 6ce0c81d8..393af42c4 100644 --- a/packages/core/src/figma/nodeToHtml.ts +++ b/packages/core/src/figma/nodeToHtml.ts @@ -162,7 +162,12 @@ interface RenderContext { } function uniqueSlug(ctx: RenderContext, name: string): string { - const base = slugify(name); + const raw = slugify(name); + // A digit-leading id ("3D Object" → "3d-object") is valid HTML but not a + // valid CSS selector — querySelector("#3d-object") throws, which breaks + // GSAP targeting and figma-motion translation. Prefix so ids stay + // selector-safe. + const base = /^[0-9]/.test(raw) ? `n${raw}` : raw; let slug = base; let n = 2; while (ctx.usedSlugs.has(slug)) slug = `${base}-${n++}`;