mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
resolveBindings: scan the full tree (boundVariables + style ids, alias chains, children) and partition exact-ID-only against the binding index before any CSS is emitted per spec 7.1 - never value matching. nodeToHtml: absolute geometry at figma bounds inside a fixed-size root, solid/linear-gradient fills, corner radius, opacity, drop shadow, blur, text styles; resolved bindings emit var(--slug, literal), unresolved bake literals with data-figma-unresolved; visible:false respected; vectors/boolean ops route to a rasterize list. hyperframes figma component: tree -> bindings -> html, rasterize fallback via Phase-1 asset export with src backfill, registry-item packaging, unresolved-binding guidance in output. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
/**
|
|
* Binding resolution pass (design spec §7.1): scan a node tree's complete
|
|
* binding set — boundVariables and style ids, alias chains honored — and
|
|
* partition against the binding index BEFORE any CSS is emitted.
|
|
*
|
|
* Exact-ID matching only. A missed link bakes a visually-correct literal;
|
|
* a wrong link silently changes color at the next brand refresh. Never
|
|
* match by value or name.
|
|
*
|
|
* NOTE: the consumer-side boundVariables shape is probe-flagged in the spec
|
|
* (§7.1 pre-build probes). Extraction here is tolerant: single alias objects
|
|
* and arrays of alias objects both count; unknown shapes are ignored rather
|
|
* than guessed at.
|
|
*/
|
|
|
|
import type { FigmaBindingRecord } from "./bindings";
|
|
import type { FigmaNodeDocument } from "./client";
|
|
import { childDocuments } from "./nodeDocument";
|
|
|
|
export interface BindingSite {
|
|
nodeId: string;
|
|
/** boundVariables property ("fills") or style slot prefixed "style:" ("style:text") */
|
|
property: string;
|
|
figmaId: string;
|
|
}
|
|
|
|
export interface ResolvedBindingSite extends BindingSite {
|
|
compositionVariableId: string;
|
|
}
|
|
|
|
export interface ResolveBindingsResult {
|
|
resolved: ResolvedBindingSite[];
|
|
unresolved: BindingSite[];
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
function aliasId(value: unknown): string | null {
|
|
if (isRecord(value) && value.type === "VARIABLE_ALIAS" && typeof value.id === "string")
|
|
return value.id;
|
|
return null;
|
|
}
|
|
|
|
function boundVariableSites(node: FigmaNodeDocument, out: BindingSite[]): void {
|
|
const bound = node.boundVariables;
|
|
if (!isRecord(bound)) return;
|
|
for (const [property, value] of Object.entries(bound)) {
|
|
const candidates = Array.isArray(value) ? value : [value];
|
|
for (const item of candidates) {
|
|
const id = aliasId(item);
|
|
if (id) out.push({ nodeId: node.id, property, figmaId: id });
|
|
}
|
|
}
|
|
}
|
|
|
|
function styleSites(node: FigmaNodeDocument, out: BindingSite[]): void {
|
|
const styles = node.styles;
|
|
if (!isRecord(styles)) return;
|
|
for (const [slot, styleId] of Object.entries(styles)) {
|
|
if (typeof styleId === "string" && styleId.length > 0)
|
|
out.push({ nodeId: node.id, property: `style:${slot}`, figmaId: styleId });
|
|
}
|
|
}
|
|
|
|
function collectSites(node: FigmaNodeDocument, out: BindingSite[]): void {
|
|
boundVariableSites(node, out);
|
|
styleSites(node, out);
|
|
for (const child of childDocuments(node)) collectSites(child, out);
|
|
}
|
|
|
|
function findInIndex(index: FigmaBindingRecord[], figmaId: string): FigmaBindingRecord | null {
|
|
for (const b of index) {
|
|
if (b.figmaId === figmaId) return b;
|
|
if (b.aliasChain?.includes(figmaId)) return b;
|
|
if (b.key !== undefined && b.key === figmaId) return b;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveBindings(
|
|
node: FigmaNodeDocument,
|
|
index: FigmaBindingRecord[],
|
|
): ResolveBindingsResult {
|
|
const sites: BindingSite[] = [];
|
|
collectSites(node, sites);
|
|
|
|
const resolved: ResolvedBindingSite[] = [];
|
|
const unresolved: BindingSite[] = [];
|
|
for (const site of sites) {
|
|
const match = findInIndex(index, site.figmaId);
|
|
if (match) resolved.push({ ...site, compositionVariableId: match.compositionVariableId });
|
|
else unresolved.push(site);
|
|
}
|
|
return { resolved, unresolved };
|
|
}
|