/** * bindVariablesToComponent * * Binds design token variables to the visual properties of a component node. * Supports fills, strokes, all padding directions, item spacing, and corner radius. * Only binds properties for which a variable ID is provided in `bindings`. * * This function should be called on each variant individually within a component * set, OR on the component set itself for properties shared by all variants. * * @param {ComponentNode | FrameNode | RectangleNode} component * The Figma node to mutate. Usually a ComponentNode or one of its children. * @param {{ * fills?: string, * strokes?: string, * paddingTop?: string, * paddingBottom?: string, * paddingLeft?: string, * paddingRight?: string, * itemSpacing?: string, * cornerRadius?: string * }} bindings * Each key is a visual property name; each value is a Figma Variable ID * (e.g. "VariableID:123:456"). Omit a key to skip binding that property. * @returns {Promise<{ mutatedNodeIds: string[] }>} * List of node IDs that were mutated (for audit/validation purposes). */ async function bindVariablesToComponent(component, bindings) { const mutatedNodeIds = [] if (!component) { return { mutatedNodeIds } } // Batch every getVariableByIdAsync call upfront in a single Promise.all rather // than awaiting per-property — the lookups are independent and IPC-bound. const floatBindings = [ ['paddingTop', 'paddingTop'], ['paddingBottom', 'paddingBottom'], ['paddingLeft', 'paddingLeft'], ['paddingRight', 'paddingRight'], ['itemSpacing', 'itemSpacing'], ['cornerRadius', 'cornerRadius'], ] const requestedIds = [] if (bindings.fills) requestedIds.push(['fills', bindings.fills]) if (bindings.strokes) requestedIds.push(['strokes', bindings.strokes]) for (const [bindingKey] of floatBindings) { if (bindings[bindingKey]) requestedIds.push([bindingKey, bindings[bindingKey]]) } const resolved = await Promise.all( requestedIds.map(([, id]) => figma.variables.getVariableByIdAsync(id)), ) const varByKey = {} for (let i = 0; i < requestedIds.length; i++) { varByKey[requestedIds[i][0]] = resolved[i] } const markMutated = () => { if (!mutatedNodeIds.includes(component.id)) { mutatedNodeIds.push(component.id) } } // --- Fills --- const fillVar = varByKey.fills if (fillVar) { const existingFills = component.fills if (Array.isArray(existingFills) && existingFills.length > 0) { // Bind the color of the first fill to the variable const boundFill = figma.variables.setBoundVariableForPaint(existingFills[0], 'color', fillVar) component.fills = [boundFill, ...existingFills.slice(1)] } else { // No existing fill — create a solid fill bound to the variable const boundFill = figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }, 'color', fillVar, ) component.fills = [boundFill] } markMutated() } // --- Strokes --- const strokeVar = varByKey.strokes if (strokeVar) { const existingStrokes = component.strokes if (Array.isArray(existingStrokes) && existingStrokes.length > 0) { const boundStroke = figma.variables.setBoundVariableForPaint( existingStrokes[0], 'color', strokeVar, ) component.strokes = [boundStroke, ...existingStrokes.slice(1)] } else { const boundStroke = figma.variables.setBoundVariableForPaint( { type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }, 'color', strokeVar, ) component.strokes = [boundStroke] } markMutated() } // --- Spacing properties (FLOAT variables bound via setBoundVariable) --- for (const [bindingKey, figmaProp] of floatBindings) { const variable = varByKey[bindingKey] if (variable) { component.setBoundVariable(figmaProp, variable) markMutated() } } return { mutatedNodeIds } }