/** * Figma Variable Bindings (StyleX-inspired API) * * @example * ```tsx * // tokens.figma.ts - with explicit values (recommended) * export const colors = defineVars({ * primary: { name: 'Colors/Gray/50', value: '#F8FAFC' }, * accent: { name: 'Colors/Blue/500', value: '#3B82F6' }, * }) * * // tokens.figma.ts - name only (value loaded from Figma) * export const colors = defineVars({ * primary: 'Colors/Gray/50', * accent: 'Colors/Blue/500', * }) * * // Card.figma.tsx * * ``` */ const VAR_SYMBOL = Symbol.for('figma.variable') /** Variable definition - either string name or object with name and value */ export type VarDef = string | { name: string; value: string } export interface FigmaVariable { [VAR_SYMBOL]: true name: string // Variable name like "Colors/Gray/50" value?: string // Fallback color value like "#F8FAFC" _resolved?: { // Filled in at render time id: string sessionID: number localID: number } } export interface ResolvedVariable { id: string sessionID: number localID: number } /** * Check if value is a Figma variable reference */ export function isVariable(value: unknown): value is FigmaVariable { return typeof value === 'object' && value !== null && VAR_SYMBOL in value } /** * Variable registry - maps names to IDs * Populated by loadVariables() before render */ const variableRegistry = new Map() /** * Load variables from Figma into registry */ export function loadVariablesIntoRegistry(variables: Array<{ id: string; name: string }>) { variableRegistry.clear() for (const v of variables) { const match = v.id.match(/VariableID:(\d+):(\d+)/) if (match) { variableRegistry.set(v.name, { id: v.id, sessionID: parseInt(match[1]!, 10), localID: parseInt(match[2]!, 10) }) } } } /** * Resolve a variable name to its ID * @throws if variable not found in registry */ export function resolveVariable(variable: FigmaVariable): ResolvedVariable { // Already resolved? if (variable._resolved) { return variable._resolved } // Check if it's an ID format (legacy support) const idMatch = variable.name.match(/^(?:VariableID:)?(\d+):(\d+)$/) if (idMatch) { const resolved = { id: `VariableID:${idMatch[1]}:${idMatch[2]}`, sessionID: parseInt(idMatch[1]!, 10), localID: parseInt(idMatch[2]!, 10) } variable._resolved = resolved return resolved } // Lookup by name const resolved = variableRegistry.get(variable.name) if (!resolved) { const available = Array.from(variableRegistry.keys()).slice(0, 5).join(', ') throw new Error( `Variable "${variable.name}" not found. ` + `Available: ${available}${variableRegistry.size > 5 ? '...' : ''}. ` + `Make sure variables are loaded before render.` ) } variable._resolved = resolved return resolved } /** * Check if variable registry is populated */ export function isRegistryLoaded(): boolean { return variableRegistry.size > 0 } /** * Get registry size (for debugging) */ export function getRegistrySize(): number { return variableRegistry.size } /** * Define Figma variables for use in styles * * @example * ```ts * // With explicit fallback values (recommended) * export const colors = defineVars({ * primary: { name: 'Colors/Gray/50', value: '#F8FAFC' }, * accent: { name: 'Colors/Blue/500', value: '#3B82F6' }, * }) * * // Name only (value loaded from Figma registry) * export const colors = defineVars({ * primary: 'Colors/Gray/50', * }) * * // Use in components: * * ``` */ export function defineVars>( vars: T ): { [K in keyof T]: FigmaVariable } { const result = {} as { [K in keyof T]: FigmaVariable } for (const [key, def] of Object.entries(vars)) { if (typeof def === 'string') { result[key as keyof T] = { [VAR_SYMBOL]: true, name: def } } else { result[key as keyof T] = { [VAR_SYMBOL]: true, name: def.name, value: def.value } } } return result } /** * Shorthand for single variable * * @example * ```ts * const primaryColor = figmaVar('Colors/Gray/50', '#F8FAFC') * ``` */ export function figmaVar(name: string, value?: string): FigmaVariable { return { [VAR_SYMBOL]: true, name, value } }