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:
Vance Ingalls
2026-07-08 00:47:13 -07:00
co-authored by Claude Fable 5
parent 80271863d3
commit def276524b
14 changed files with 623 additions and 173 deletions
+35
View File
@@ -0,0 +1,35 @@
/**
* THE slug for composition-variable ids → CSS custom-property names. Shared
* by the figma importer (emits `var(--<slug>, literal)`) and the runtime
* (defines `--<slug>` from declared variables) — one function so the two
* sides can never drift. The slug is lossy (case-folded, symbol runs
* collapse to "-"), so distinct ids CAN collide; callers surface
* detectSlugCollisions() as a warning rather than silently merging.
*/
export function slugify(name: string): string {
const slug = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return slug.length > 0 ? slug : "node";
}
export function cssVariableName(id: string): string {
return `--${slugify(id)}`;
}
/** Groups of distinct ids that collapse to the same CSS variable name. */
export function detectSlugCollisions(ids: Iterable<string>): string[][] {
const bySlug = new Map<string, string[]>();
for (const id of ids) {
const slug = cssVariableName(id);
const group = bySlug.get(slug);
if (group) {
if (!group.includes(id)) group.push(id);
} else {
bySlug.set(slug, [id]);
}
}
return [...bySlug.values()].filter((g) => g.length > 1);
}