/** * createSemanticTokens * * Creates a batch of Figma variables in the given collection, one per entry in * `tokenMap`. Supports raw values, variable alias references, code syntax, and * scopes. Returns a map of token name → Variable for use in subsequent steps. * * @param {VariableCollection} collection - The target variable collection. * @param {Record} modeIds - Map of {modeName: modeId} from createVariableCollection. * @param {Array<{ * name: string, * type: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN', * values: Record, * scopes?: VariableScope[], * codeSyntax?: {WEB?: string, ANDROID?: string, iOS?: string} * }>} tokenMap - Ordered list of token definitions. * - `name`: Variable name using slash hierarchy (e.g. "color/bg/primary"). * - `type`: Figma variable type. * - `values`: Map of {modeName: value}. Values can be raw (hex string for COLOR, * number for FLOAT) or alias objects {type: 'VARIABLE_ALIAS', id: variableId}. * For COLOR, raw values are accepted as hex strings ("#rrggbb" or "#rrggbbaa") * and converted to {r, g, b, a} automatically. * - `scopes`: Array of VariableScope strings. Omit to use [] (hidden/primitive). * - `codeSyntax`: Platform code syntax strings. Omit to skip. * @param {string} [runId] - Optional dsb_run_id to tag every variable. * @returns {Promise<{variables: Record}>} * `variables` maps each token name to its created Variable object. */ async function createSemanticTokens(collection, modeIds, tokenMap, runId) { const variables = {} for (const token of tokenMap) { // Create the variable const variable = figma.variables.createVariable(token.name, collection, token.type) // Tag for cleanup variable.setPluginData('dsb_key', `variable/${token.name}`) if (runId) { variable.setPluginData('dsb_run_id', runId) } // Set values for each mode for (const [modeName, rawValue] of Object.entries(token.values)) { const modeId = modeIds[modeName] if (!modeId) { throw new Error( `createSemanticTokens: mode "${modeName}" not found in modeIds for token "${token.name}". ` + `Available modes: ${Object.keys(modeIds).join(', ')}`, ) } let value = rawValue // Convert hex strings to Figma RGBA for COLOR type if (token.type === 'COLOR' && typeof rawValue === 'string' && rawValue.startsWith('#')) { value = hexToFigmaColor(rawValue) } variable.setValueForMode(modeId, value) } // Set scopes (default: empty array = hidden from property pickers / primitives) variable.scopes = token.scopes || [] // Set code syntax per platform if (token.codeSyntax) { if (token.codeSyntax.WEB) { variable.setVariableCodeSyntax('WEB', token.codeSyntax.WEB) } if (token.codeSyntax.ANDROID) { variable.setVariableCodeSyntax('ANDROID', token.codeSyntax.ANDROID) } if (token.codeSyntax.iOS) { variable.setVariableCodeSyntax('iOS', token.codeSyntax.iOS) } } variables[token.name] = variable } return { variables } } /** * Converts a hex color string to a Figma RGBA object. * Supports "#rgb", "#rrggbb", and "#rrggbbaa". * * @param {string} hex * @returns {{ r: number, g: number, b: number, a: number }} */ function hexToFigmaColor(hex) { let h = hex.replace('#', '') // Expand shorthand #rgb → #rrggbb if (h.length === 3) { h = h .split('') .map((c) => c + c) .join('') } const r = parseInt(h.substring(0, 2), 16) / 255 const g = parseInt(h.substring(2, 4), 16) / 255 const b = parseInt(h.substring(4, 6), 16) / 255 const a = h.length === 8 ? parseInt(h.substring(6, 8), 16) / 255 : 1 return { r, g, b, a } }