├── Variables Import & Export Icon.png ├── Variables Import & Export Banner.png ├── manifest.json ├── export.html ├── import.html └── code.js /Variables Import & Export Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jake-figma/variables-import-export/HEAD/Variables Import & Export Icon.png -------------------------------------------------------------------------------- /Variables Import & Export Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jake-figma/variables-import-export/HEAD/Variables Import & Export Banner.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Variables Import Export", 3 | "id": "1225498390710809905", 4 | "api": "1.0.0", 5 | "editorType": ["figma"], 6 | "permissions": [], 7 | "main": "code.js", 8 | "menu": [ 9 | { "command": "import", "name": "Import Variables" }, 10 | { "command": "export", "name": "Export Variables" } 11 | ], 12 | "ui": { "import": "import.html", "export": "export.html" } 13 | } 14 | -------------------------------------------------------------------------------- /export.html: -------------------------------------------------------------------------------- 1 | 71 |
72 | 73 | 77 |
78 | 93 | -------------------------------------------------------------------------------- /import.html: -------------------------------------------------------------------------------- 1 | 87 |
88 |
89 | 90 | 91 |
92 | 93 |
94 | 95 | 96 |
97 | 98 | 132 | 133 |
134 | 135 | 264 | -------------------------------------------------------------------------------- /code.js: -------------------------------------------------------------------------------- 1 | console.clear(); 2 | 3 | function createCollection(selectedCollection, selectedMode) { 4 | // collection exists 5 | if (selectedCollection.id) { 6 | const collection = figma.variables.getVariableCollectionById( 7 | selectedCollection.id 8 | ); 9 | let modeId = selectedMode.id; 10 | // mode exists 11 | if (modeId) { 12 | return { collection, modeId }; 13 | } 14 | 15 | // otherwise create new mode 16 | const newMode = collection.addMode(selectedMode.name); 17 | return { collection, modeId: newMode }; 18 | } 19 | 20 | // collection doesn't exist, so mode doesn't exist 21 | const collection = figma.variables.createVariableCollection( 22 | selectedCollection.name 23 | ); 24 | const modeId = collection.modes[0].modeId; 25 | collection.renameMode(modeId, selectedMode.name); 26 | return { collection, modeId }; 27 | } 28 | 29 | function createToken(variableMap, collection, modeId, type, name, value) { 30 | const existingCollection = variableMap[collection.id]; 31 | let token = existingCollection ? existingCollection[name] : null; 32 | 33 | if (!token) { 34 | token = figma.variables.createVariable(name, collection.id, type); 35 | } 36 | token.setValueForMode(modeId, value); 37 | return token; 38 | } 39 | 40 | function createVariable( 41 | variableMap, 42 | collection, 43 | modeId, 44 | key, 45 | valueKey, 46 | tokens 47 | ) { 48 | const token = tokens[valueKey]; 49 | return createToken(variableMap, collection, modeId, token.resolvedType, key, { 50 | type: "VARIABLE_ALIAS", 51 | id: `${token.id}`, 52 | }); 53 | } 54 | 55 | function getExistingCollectionsAndModes() { 56 | const collections = figma.variables 57 | .getLocalVariableCollections() 58 | .reduce((into, collection) => { 59 | into[collection.name] = { 60 | name: collection.name, 61 | id: collection.id, 62 | defaultModeId: collection.defaultModeId, 63 | modes: collection.modes, 64 | }; 65 | return into; 66 | }, {}); 67 | 68 | figma.ui.postMessage({ 69 | type: "LOAD_COLLECTIONS", 70 | collections, 71 | }); 72 | } 73 | 74 | function importJSONFile({ selectedCollection, selectedMode, body }) { 75 | const json = JSON.parse(body); 76 | console.log("IMPORT"); 77 | const { collection, modeId } = createCollection( 78 | selectedCollection, 79 | selectedMode 80 | ); 81 | const variableMap = loadExistingVariableMap(); 82 | 83 | const aliases = {}; 84 | const tokens = {}; 85 | Object.entries(json).forEach(([key, object]) => { 86 | traverseToken({ 87 | variableMap, 88 | collection, 89 | modeId, 90 | type: json.$type, 91 | key, 92 | object, 93 | tokens, 94 | aliases, 95 | }); 96 | }); 97 | processAliases({ variableMap, collection, modeId, aliases, tokens }); 98 | } 99 | 100 | function loadExistingVariableMap() { 101 | const variables = figma.variables.getLocalVariables(); 102 | const map = {}; 103 | variables.forEach((variable) => { 104 | map[variable.variableCollectionId] = 105 | map[variable.variableCollectionId] || {}; 106 | map[variable.variableCollectionId][variable.name] = variable; 107 | }); 108 | return map; 109 | } 110 | 111 | function processAliases({ variableMap, collection, modeId, aliases, tokens }) { 112 | aliases = Object.values(aliases); 113 | let generations = aliases.length; 114 | while (aliases.length && generations > 0) { 115 | for (let i = 0; i < aliases.length; i++) { 116 | const { key, type, valueKey } = aliases[i]; 117 | const token = tokens[valueKey]; 118 | if (token) { 119 | aliases.splice(i, 1); 120 | tokens[key] = createVariable( 121 | variableMap, 122 | collection, 123 | modeId, 124 | key, 125 | valueKey, 126 | tokens 127 | ); 128 | } 129 | } 130 | generations--; 131 | } 132 | } 133 | 134 | function isAlias(value) { 135 | return value.toString().trim().charAt(0) === "{"; 136 | } 137 | 138 | function traverseToken({ 139 | variableMap, 140 | collection, 141 | modeId, 142 | type, 143 | key, 144 | object, 145 | tokens, 146 | aliases, 147 | }) { 148 | type = type || object.$type; 149 | // if key is a meta field, move on 150 | if (key.charAt(0) === "$") { 151 | return; 152 | } 153 | if (object.$value !== undefined) { 154 | if (isAlias(object.$value)) { 155 | const valueKey = object.$value 156 | .trim() 157 | .replace(/\./g, "/") 158 | .replace(/[\{\}]/g, ""); 159 | if (tokens[valueKey]) { 160 | tokens[key] = createVariable( 161 | variableMap, 162 | collection, 163 | modeId, 164 | key, 165 | valueKey, 166 | tokens 167 | ); 168 | } else { 169 | aliases[key] = { 170 | key, 171 | type, 172 | valueKey, 173 | }; 174 | } 175 | } else if (type === "color") { 176 | tokens[key] = createToken( 177 | variableMap, 178 | collection, 179 | modeId, 180 | "COLOR", 181 | key, 182 | parseColor(object.$value) 183 | ); 184 | } else if (type === "number") { 185 | tokens[key] = createToken( 186 | variableMap, 187 | collection, 188 | modeId, 189 | "FLOAT", 190 | key, 191 | object.$value 192 | ); 193 | } else { 194 | console.log("unsupported type", type, object); 195 | } 196 | } else { 197 | Object.entries(object).forEach(([key2, object2]) => { 198 | if (key2.charAt(0) !== "$") { 199 | traverseToken({ 200 | variableMap, 201 | collection, 202 | modeId, 203 | type, 204 | key: `${key}/${key2}`, 205 | object: object2, 206 | tokens, 207 | aliases, 208 | }); 209 | } 210 | }); 211 | } 212 | } 213 | 214 | function exportToJSON() { 215 | const collections = figma.variables.getLocalVariableCollections(); 216 | const files = []; 217 | collections.forEach((collection) => 218 | files.push(...processCollection(collection)) 219 | ); 220 | figma.ui.postMessage({ type: "EXPORT_RESULT", files }); 221 | } 222 | 223 | function processCollection({ name, modes, variableIds }) { 224 | const files = []; 225 | modes.forEach((mode) => { 226 | const file = { fileName: `${name}.${mode.name}.tokens.json`, body: {} }; 227 | variableIds.forEach((variableId) => { 228 | const { name, resolvedType, valuesByMode } = 229 | figma.variables.getVariableById(variableId); 230 | const value = valuesByMode[mode.modeId]; 231 | if (value !== undefined && ["COLOR", "FLOAT"].includes(resolvedType)) { 232 | let obj = file.body; 233 | name.split("/").forEach((groupName) => { 234 | obj[groupName] = obj[groupName] || {}; 235 | obj = obj[groupName]; 236 | }); 237 | obj.$type = resolvedType === "COLOR" ? "color" : "number"; 238 | if (value.type === "VARIABLE_ALIAS") { 239 | obj.$value = `{${figma.variables 240 | .getVariableById(value.id) 241 | .name.replace(/\//g, ".")}}`; 242 | } else { 243 | obj.$value = resolvedType === "COLOR" ? rgbToHex(value) : value; 244 | } 245 | } 246 | }); 247 | files.push(file); 248 | }); 249 | return files; 250 | } 251 | 252 | figma.ui.onmessage = (e) => { 253 | console.log("code received message", e); 254 | if (e.type === "IMPORT") { 255 | const { selectedCollection, selectedMode, body } = e; 256 | importJSONFile({ selectedCollection, selectedMode, body }); 257 | getExistingCollectionsAndModes(); 258 | } else if (e.type === "EXPORT") { 259 | exportToJSON(); 260 | } 261 | }; 262 | if (figma.command === "import") { 263 | figma.showUI(__uiFiles__["import"], { 264 | width: 500, 265 | height: 500, 266 | themeColors: true, 267 | }); 268 | getExistingCollectionsAndModes(); 269 | } else if (figma.command === "export") { 270 | figma.showUI(__uiFiles__["export"], { 271 | width: 500, 272 | height: 500, 273 | themeColors: true, 274 | }); 275 | } 276 | 277 | function rgbToHex({ r, g, b, a }) { 278 | if (a !== 1) { 279 | return `rgba(${[r, g, b] 280 | .map((n) => Math.round(n * 255)) 281 | .join(", ")}, ${a.toFixed(4)})`; 282 | } 283 | const toHex = (value) => { 284 | const hex = Math.round(value * 255).toString(16); 285 | return hex.length === 1 ? "0" + hex : hex; 286 | }; 287 | 288 | const hex = [toHex(r), toHex(g), toHex(b)].join(""); 289 | return `#${hex}`; 290 | } 291 | 292 | function parseColor(color) { 293 | color = color.trim(); 294 | const rgbRegex = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/; 295 | const rgbaRegex = 296 | /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d.]+)\s*\)$/; 297 | const hslRegex = /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/; 298 | const hslaRegex = 299 | /^hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*([\d.]+)\s*\)$/; 300 | const hexRegex = /^#([A-Fa-f0-9]{3}){1,2}$/; 301 | const floatRgbRegex = 302 | /^\{\s*r:\s*[\d\.]+,\s*g:\s*[\d\.]+,\s*b:\s*[\d\.]+(,\s*opacity:\s*[\d\.]+)?\s*\}$/; 303 | 304 | if (rgbRegex.test(color)) { 305 | const [, r, g, b] = color.match(rgbRegex); 306 | return { r: parseInt(r) / 255, g: parseInt(g) / 255, b: parseInt(b) / 255 }; 307 | } else if (rgbaRegex.test(color)) { 308 | const [, r, g, b, a] = color.match(rgbaRegex); 309 | return { 310 | r: parseInt(r) / 255, 311 | g: parseInt(g) / 255, 312 | b: parseInt(b) / 255, 313 | a: parseFloat(a), 314 | }; 315 | } else if (hslRegex.test(color)) { 316 | const [, h, s, l] = color.match(hslRegex); 317 | return hslToRgbFloat(parseInt(h), parseInt(s) / 100, parseInt(l) / 100); 318 | } else if (hslaRegex.test(color)) { 319 | const [, h, s, l, a] = color.match(hslaRegex); 320 | return Object.assign( 321 | hslToRgbFloat(parseInt(h), parseInt(s) / 100, parseInt(l) / 100), 322 | { a: parseFloat(a) } 323 | ); 324 | } else if (hexRegex.test(color)) { 325 | const hexValue = color.substring(1); 326 | const expandedHex = 327 | hexValue.length === 3 328 | ? hexValue 329 | .split("") 330 | .map((char) => char + char) 331 | .join("") 332 | : hexValue; 333 | return { 334 | r: parseInt(expandedHex.slice(0, 2), 16) / 255, 335 | g: parseInt(expandedHex.slice(2, 4), 16) / 255, 336 | b: parseInt(expandedHex.slice(4, 6), 16) / 255, 337 | }; 338 | } else if (floatRgbRegex.test(color)) { 339 | return JSON.parse(color); 340 | } else { 341 | throw new Error("Invalid color format"); 342 | } 343 | } 344 | 345 | function hslToRgbFloat(h, s, l) { 346 | const hue2rgb = (p, q, t) => { 347 | if (t < 0) t += 1; 348 | if (t > 1) t -= 1; 349 | if (t < 1 / 6) return p + (q - p) * 6 * t; 350 | if (t < 1 / 2) return q; 351 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; 352 | return p; 353 | }; 354 | 355 | if (s === 0) { 356 | return { r: l, g: l, b: l }; 357 | } 358 | 359 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s; 360 | const p = 2 * l - q; 361 | const r = hue2rgb(p, q, (h + 1 / 3) % 1); 362 | const g = hue2rgb(p, q, h % 1); 363 | const b = hue2rgb(p, q, (h - 1 / 3) % 1); 364 | 365 | return { r, g, b }; 366 | } 367 | --------------------------------------------------------------------------------