From 1736656281d23db7eecb46584b3c1d98513e2bdb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 18 Nov 2025 16:15:54 -0500 Subject: [PATCH 1/7] Sync package-lock.json --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ec4067..56da94e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "react", - "version": "1.0.0", + "name": "wpds-tokens-figma-plugin", + "version": "5.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "react", - "version": "1.0.0", + "name": "wpds-tokens-figma-plugin", + "version": "5.0.0", "license": "MIT License", "dependencies": { "culori": "^4.0.2", From 322f7e3f7aa997a0cee51eb72eb13e69c409d12f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 18 Nov 2025 16:18:44 -0500 Subject: [PATCH 2/7] Add support for updated tokens conventions (dimension) - Removes assumptions around "light" as a default mode, since current theming algorithm does not have explicit dark or light modes. Instead uses "default" as default mode - Updates to remove handling of aliases to primitive values, as primitive values are no longer included in Figma JSON output - Adds handling for imported dimension values (padding, gap) as numeric values - Creates separate collections based on top-level grouping (i.e. "Color" and "Dimension"). This is because each grouping may have their own grouping-specific modes. For example, density should only affect dimension values. --- plugin-src/code.ts | 112 +++++++++++++++++++++------------------------ types.ts | 6 +-- ui-src/App.tsx | 11 ++++- 3 files changed, 64 insertions(+), 65 deletions(-) diff --git a/plugin-src/code.ts b/plugin-src/code.ts index db0d4ce..789ea8a 100644 --- a/plugin-src/code.ts +++ b/plugin-src/code.ts @@ -6,7 +6,8 @@ figma.showUI(__html__, { themeColors: true, height: 300 }); // alias variables correctly. const allImportedVariables: Record = {}; -const DEFAULT_COLOR_MODE = "light"; +const LEGACY_DEFAULT_MODE = "light"; +const DEFAULT_MODE = "default"; function isAliasValue( value: ParsedTokenValue[keyof ParsedTokenValue] @@ -22,6 +23,26 @@ function isValidColorValue(value: any): value is ParsedTokenValue { ); } +function getResolvedType(value: any): VariableResolvedDataType | undefined { + if (isValidColorValue(value)) { + return "COLOR"; + } else if (typeof value === "number") { + return "FLOAT"; + } +} + +function groupByCollection(tokens: ParsedTokens): Record { + return Object.entries(tokens).reduce>( + (result, [tokenName, tokenData]) => { + const collection = tokenName.split("/")[0]; + result[collection] = result[collection] || {}; + result[collection][tokenName] = tokenData; + return result; + }, + {} + ); +} + async function updateCollection(args: { tokens: ParsedTokens; collectionName: string; @@ -48,6 +69,10 @@ async function updateCollection(args: { // Get existing modes in the collection for (const mode of collection.modes) { + if (mode.name === LEGACY_DEFAULT_MODE) { + collection.renameMode(mode.modeId, DEFAULT_MODE); + } + modesInCollectionBeforeImporting[mode.name] = mode.modeId; } @@ -64,9 +89,19 @@ async function updateCollection(args: { let variable = variablesInCollectionBeforeImporting[tokenName]; + // Don't save variables where we can't determine the resolved type + const resolvedType = getResolvedType(value["."]); + if (!resolvedType) { + continue; + } + if (!variable) { // Create new variable - variable = figma.variables.createVariable(tokenName, collection, "COLOR"); + variable = figma.variables.createVariable( + tokenName, + collection, + resolvedType + ); } if (description) { @@ -83,12 +118,16 @@ async function updateCollection(args: { variable.scopes = ["STROKE_COLOR", "EFFECT_COLOR"]; } else if (/color\/semantic\/background/gi.test(tokenName)) { variable.scopes = ["FRAME_FILL", "SHAPE_FILL"]; + } else if (/dimension\/semantic\/(padding|gap)/gi.test(tokenName)) { + variable.scopes = ["GAP"]; + } else if (/dimension\/semantic/gi.test(tokenName)) { + variable.scopes = ["WIDTH_HEIGHT"]; } else { variable.scopes = []; } for (const [modeName, modeValue] of Object.entries(value)) { - const computedModeName = modeName === "." ? DEFAULT_COLOR_MODE : modeName; + const computedModeName = modeName === "." ? DEFAULT_MODE : modeName; if (!(computedModeName in modesInCollectionBeforeImporting)) { modesInCollectionBeforeImporting[computedModeName] = @@ -101,10 +140,6 @@ async function updateCollection(args: { continue; } - if (!isValidColorValue(modeValue)) { - continue; - } - variable.setValueForMode( modesInCollectionBeforeImporting[computedModeName], modeValue @@ -115,51 +150,7 @@ async function updateCollection(args: { variablesUpdatedDuringImport[tokenName] = true; } - // Pass 2: create or update aliases - for (const [tokenName, tokenData] of Object.entries(tokens)) { - const { value } = tokenData; - - const variable = allImportedVariables[tokenName]; - if (!variable) { - console.log( - "Something is off — this variable should have already been created" - ); - continue; - } - - for (const [modeName, modeValue] of Object.entries(value)) { - const computedModeName = modeName === "." ? DEFAULT_COLOR_MODE : modeName; - - if (!(computedModeName in modesInCollectionBeforeImporting)) { - console.log( - "Something is off — this mode should have already been created" - ); - continue; - } - - // Second pass: only save aliased values - if (!isAliasValue(modeValue)) { - continue; - } - - const matchAliasTokenName = /^\{(.*)\}$/.exec(modeValue); - const aliasTokenName = matchAliasTokenName && matchAliasTokenName[1]; - - if (!aliasTokenName || !allImportedVariables[aliasTokenName]) { - continue; - } - - variable.setValueForMode( - modesInCollectionBeforeImporting[computedModeName], - { - type: "VARIABLE_ALIAS", - id: allImportedVariables[aliasTokenName].id, - } - ); - } - } - - // Pass 3: fallback missing mode values to the default mode value + // Pass 2: fallback missing mode values to the default mode value for (const [tokenName, tokenData] of Object.entries(tokens)) { const { value } = tokenData; @@ -174,7 +165,7 @@ async function updateCollection(args: { const modesMissingValues = new Set(collection.modes.map((m) => m.modeId)); for (const [modeName] of Object.entries(value)) { - const computedModeName = modeName === "." ? DEFAULT_COLOR_MODE : modeName; + const computedModeName = modeName === "." ? DEFAULT_MODE : modeName; if (!(computedModeName in modesInCollectionBeforeImporting)) { console.log( @@ -191,9 +182,7 @@ async function updateCollection(args: { for (const modeId of modesMissingValues) { variable.setValueForMode( modeId, - variable.valuesByMode[ - modesInCollectionBeforeImporting[DEFAULT_COLOR_MODE] - ] + variable.valuesByMode[modesInCollectionBeforeImporting[DEFAULT_MODE]] ); } } @@ -220,11 +209,14 @@ async function updateCollection(args: { figma.ui.onmessage = async (msg) => { if (msg.type === "import-tokens") { const parsedTokens: ParsedTokens = msg.parsedTokens; + const groupedTokens = groupByCollection(parsedTokens); - await updateCollection({ - tokens: parsedTokens, - collectionName: "WPDS Tokens", - }); + for (const [collectionName, tokens] of Object.entries(groupedTokens)) { + await updateCollection({ + tokens: tokens, + collectionName: `WPDS Tokens/${collectionName}`, + }); + } } figma.closePlugin(); diff --git a/types.ts b/types.ts index 489e8f1..a4b16be 100644 --- a/types.ts +++ b/types.ts @@ -11,10 +11,10 @@ export interface RGBA { readonly a: number; } -type ColorModes = "light" | "dark" | "."; +type TokenMode = "."; export type ImportedTokenValue = { - [key in ColorModes]?: string; + [key in TokenMode]?: string; }; export interface ImportedTokens { @@ -25,7 +25,7 @@ export interface ImportedTokens { } export type ParsedTokenValue = { - [key in ColorModes]?: RGB | RGBA | string; + [key in TokenMode]?: RGB | RGBA | string | number; }; export interface ParsedTokens { [key: string]: { diff --git a/ui-src/App.tsx b/ui-src/App.tsx index dc6c6a8..a320cbf 100644 --- a/ui-src/App.tsx +++ b/ui-src/App.tsx @@ -23,8 +23,8 @@ function App() { const parsedTokens: ParsedTokens = {}; for (const [tokenName, tokenObject] of Object.entries(tokens)) { - // For now, only import colors. - if (!/color/gi.test(tokenName)) { + // Limit to supported token types + if (!/^(color|dimension)/gi.test(tokenName)) { continue; } @@ -54,6 +54,13 @@ function App() { b: Math.max(Math.min(converted.b, 1), 0), a: converted.alpha ?? 1, }; + } else if (/dimension/gi.test(tokenName)) { + if (!/px$/.test(modeValue)) { + console.warn(`Invalid dimension value: ${modeValue}`); + continue; + } + + computedModeValue = Number(modeValue.replace("px", "")); } if (!computedModeValue) { From f44f351d2ecd9b5eefa34a3c04da8b04d467f939 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 21 Nov 2025 10:55:18 -0500 Subject: [PATCH 3/7] Remove "Tokens/" from collection name --- plugin-src/code.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin-src/code.ts b/plugin-src/code.ts index 789ea8a..1c564d6 100644 --- a/plugin-src/code.ts +++ b/plugin-src/code.ts @@ -214,7 +214,7 @@ figma.ui.onmessage = async (msg) => { for (const [collectionName, tokens] of Object.entries(groupedTokens)) { await updateCollection({ tokens: tokens, - collectionName: `WPDS Tokens/${collectionName}`, + collectionName: `WPDS ${collectionName}`, }); } } From 370ea58282f4477ec6a07616c8801ba5e4c630bb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 21 Nov 2025 10:59:05 -0500 Subject: [PATCH 4/7] Restore alias reference value handling While we don't export primitive values, alias handling should be a basic functionality to support that we might leverage in the future --- plugin-src/code.ts | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/plugin-src/code.ts b/plugin-src/code.ts index 1c564d6..b5642f9 100644 --- a/plugin-src/code.ts +++ b/plugin-src/code.ts @@ -150,7 +150,51 @@ async function updateCollection(args: { variablesUpdatedDuringImport[tokenName] = true; } - // Pass 2: fallback missing mode values to the default mode value + // Pass 2: create or update aliases + for (const [tokenName, tokenData] of Object.entries(tokens)) { + const { value } = tokenData; + + const variable = allImportedVariables[tokenName]; + if (!variable) { + console.log( + "Something is off — this variable should have already been created" + ); + continue; + } + + for (const [modeName, modeValue] of Object.entries(value)) { + const computedModeName = modeName === "." ? DEFAULT_MODE : modeName; + + if (!(computedModeName in modesInCollectionBeforeImporting)) { + console.log( + "Something is off — this mode should have already been created" + ); + continue; + } + + // Second pass: only save aliased values + if (!isAliasValue(modeValue)) { + continue; + } + + const matchAliasTokenName = /^\{(.*)\}$/.exec(modeValue); + const aliasTokenName = matchAliasTokenName && matchAliasTokenName[1]; + + if (!aliasTokenName || !allImportedVariables[aliasTokenName]) { + continue; + } + + variable.setValueForMode( + modesInCollectionBeforeImporting[computedModeName], + { + type: "VARIABLE_ALIAS", + id: allImportedVariables[aliasTokenName].id, + } + ); + } + } + + // Pass 3: fallback missing mode values to the default mode value for (const [tokenName, tokenData] of Object.entries(tokens)) { const { value } = tokenData; From 86ee916bb85f38c7b13129a7ac2c1359ea38b7c5 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 21 Nov 2025 11:47:11 -0500 Subject: [PATCH 5/7] Update expectations to not assume "semantic" grouping Being removed from output --- plugin-src/code.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin-src/code.ts b/plugin-src/code.ts index b5642f9..766b773 100644 --- a/plugin-src/code.ts +++ b/plugin-src/code.ts @@ -112,15 +112,15 @@ async function updateCollection(args: { variable.hiddenFromPublishing = /primitive/.test(tokenName); // Update scopes - if (/color\/semantic\/foreground/gi.test(tokenName)) { + if (/color\/foreground/gi.test(tokenName)) { variable.scopes = ["TEXT_FILL", "SHAPE_FILL", "STROKE_COLOR"]; - } else if (/color\/semantic\/stroke/gi.test(tokenName)) { + } else if (/color\/stroke/gi.test(tokenName)) { variable.scopes = ["STROKE_COLOR", "EFFECT_COLOR"]; - } else if (/color\/semantic\/background/gi.test(tokenName)) { + } else if (/color\/background/gi.test(tokenName)) { variable.scopes = ["FRAME_FILL", "SHAPE_FILL"]; - } else if (/dimension\/semantic\/(padding|gap)/gi.test(tokenName)) { + } else if (/dimension\/(padding|gap)/gi.test(tokenName)) { variable.scopes = ["GAP"]; - } else if (/dimension\/semantic/gi.test(tokenName)) { + } else if (/dimension/gi.test(tokenName)) { variable.scopes = ["WIDTH_HEIGHT"]; } else { variable.scopes = []; From e260a6b884626cea6b7f5b3764e20b9dc8795ff8 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 5 Dec 2025 11:35:23 -0500 Subject: [PATCH 6/7] Remove redundant primitive handling We no longer import primitive tokens --- plugin-src/code.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin-src/code.ts b/plugin-src/code.ts index 766b773..194b244 100644 --- a/plugin-src/code.ts +++ b/plugin-src/code.ts @@ -109,7 +109,6 @@ async function updateCollection(args: { } else { variable.description = ""; } - variable.hiddenFromPublishing = /primitive/.test(tokenName); // Update scopes if (/color\/foreground/gi.test(tokenName)) { From af26c937e360f94b4f69165e1450b8f7a6808cb2 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 5 Dec 2025 11:40:21 -0500 Subject: [PATCH 7/7] Update import to align to new export conventions --- plugin-src/code.ts | 11 +++-------- ui-src/App.tsx | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/plugin-src/code.ts b/plugin-src/code.ts index 194b244..a4e3f6b 100644 --- a/plugin-src/code.ts +++ b/plugin-src/code.ts @@ -6,7 +6,6 @@ figma.showUI(__html__, { themeColors: true, height: 300 }); // alias variables correctly. const allImportedVariables: Record = {}; -const LEGACY_DEFAULT_MODE = "light"; const DEFAULT_MODE = "default"; function isAliasValue( @@ -69,10 +68,6 @@ async function updateCollection(args: { // Get existing modes in the collection for (const mode of collection.modes) { - if (mode.name === LEGACY_DEFAULT_MODE) { - collection.renameMode(mode.modeId, DEFAULT_MODE); - } - modesInCollectionBeforeImporting[mode.name] = mode.modeId; } @@ -111,11 +106,11 @@ async function updateCollection(args: { } // Update scopes - if (/color\/foreground/gi.test(tokenName)) { + if (/color\/fg/gi.test(tokenName)) { variable.scopes = ["TEXT_FILL", "SHAPE_FILL", "STROKE_COLOR"]; } else if (/color\/stroke/gi.test(tokenName)) { variable.scopes = ["STROKE_COLOR", "EFFECT_COLOR"]; - } else if (/color\/background/gi.test(tokenName)) { + } else if (/color\/bg/gi.test(tokenName)) { variable.scopes = ["FRAME_FILL", "SHAPE_FILL"]; } else if (/dimension\/(padding|gap)/gi.test(tokenName)) { variable.scopes = ["GAP"]; @@ -257,7 +252,7 @@ figma.ui.onmessage = async (msg) => { for (const [collectionName, tokens] of Object.entries(groupedTokens)) { await updateCollection({ tokens: tokens, - collectionName: `WPDS ${collectionName}`, + collectionName, }); } } diff --git a/ui-src/App.tsx b/ui-src/App.tsx index a320cbf..a421ddd 100644 --- a/ui-src/App.tsx +++ b/ui-src/App.tsx @@ -24,7 +24,7 @@ function App() { for (const [tokenName, tokenObject] of Object.entries(tokens)) { // Limit to supported token types - if (!/^(color|dimension)/gi.test(tokenName)) { + if (!/^wpds-(color|dimension)/gi.test(tokenName)) { continue; }