-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Darkside] Figma plugin for updating variables (#3191)
* feat: Implemented figma plugin * feat: Can now detect production/development build * feat: Now supports remote fetch for config * misc: Stop reset from running by default * feat:Semantic value handling * feat: Pre-semantic implementation * feat: Finished plugin API for semantic variables * feat: Use this for Figma plugin extension * bug: Fixed semantic collection naming * bug: Handle reset correctly * misc: avoid resetting when updating * memo: Added comments * bug: Ignore plugin buildfile from lint * bug: Fix support for dynamic-page load in figma * 📝 Better naming, simplified code-def syntax * feat: Scoped ignorePattern for plugin.js * memo: better logs for completing update * Update @navikt/core/tokens/darkside/figma/plugin/AkselVariablesInterface.ts Co-authored-by: Halvor Haugan <[email protected]> * Update @navikt/core/tokens/darkside/figma/plugin/AkselVariablesInterface.ts Co-authored-by: Halvor Haugan <[email protected]> * memo: Removed extra space for info logging * refactor: Simplify for-loop for semantic color modes * refactor: Inline creation of mode, collection and variable * refactor: readable mapping variables * feat: Better error-handling if variable is null * refactor: Remove redundant getModes function * refactor: Remove redundant getModes function * refactor: Remove plugin watch function --------- Co-authored-by: Halvor Haugan <[email protected]>
- Loading branch information
1 parent
87aeb94
commit 678fa66
Showing
12 changed files
with
773 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
figma-config.json | ||
figma-config.json | ||
plugin.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
234 changes: 234 additions & 0 deletions
234
@navikt/core/tokens/darkside/figma/plugin/AkselVariablesInterface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
import _config from "../../../figma-config.json"; | ||
import { FigmaConfigEntry, FigmaTokenConfig } from "../figma-config.types"; | ||
import { FigmaPluginInterface } from "./FigmaPluginInterface"; | ||
|
||
type ScopedFigmaTokenConfig = Omit<FigmaTokenConfig, "version" | "timestamp">; | ||
|
||
export class AkselVariablesInterface { | ||
private Figma: FigmaPluginInterface; | ||
private config: FigmaTokenConfig; | ||
private meta: Pick<FigmaTokenConfig, "version" | "timestamp">; | ||
private remoteConfigURL = | ||
"https://cdn.nav.no/designsystem/@navikt/tokens/figma-config.json"; | ||
|
||
constructor() { | ||
const config = _config as FigmaTokenConfig; | ||
this.config = config; | ||
this.meta = { timestamp: config.timestamp, version: config.version }; | ||
this.Figma = new FigmaPluginInterface(); | ||
} | ||
|
||
/** | ||
* Because of "dynamic-page" loading in Figma, | ||
* we need to initialize the plugin trough async methods. | ||
* This method should be called before any other methods. | ||
*/ | ||
async init(): Promise<void> { | ||
await this.Figma.init(); | ||
} | ||
|
||
async useRemoteConfig(): Promise<void> { | ||
const newConfig = await fetch(this.remoteConfigURL) | ||
.then((res) => res.json()) | ||
.catch((err) => { | ||
console.error(err); | ||
throw new Error("Error fetching config from CDN 😱"); | ||
}); | ||
this.meta = { timestamp: newConfig.timestamp, version: newConfig.version }; | ||
|
||
this.config = newConfig; | ||
} | ||
|
||
updateVariables(): void { | ||
this.updateGlobalColorCollection(this.config.colors.light.global); | ||
this.updateGlobalColorCollection(this.config.colors.dark.global); | ||
this.updateSemanticColorCollection(this.config.colors); | ||
this.updateScaleCollection(this.config.radius); | ||
this.updateScaleCollection(this.config.spacing); | ||
console.info("Variables updated!"); | ||
} | ||
|
||
private updateGlobalColorCollection( | ||
globalColorTheme: FigmaConfigEntry, | ||
): void { | ||
const collection = | ||
this.Figma.getCollection(globalColorTheme.name) ?? | ||
this.Figma.createCollection(globalColorTheme.name); | ||
|
||
/** | ||
* Correctly sorts the token-scale for global colors | ||
* 000 - 100 - 200... etc. | ||
*/ | ||
const getLastNumber = (name: string) => { | ||
const matches = name.match(/\d+/g); | ||
return matches ? parseInt(matches[matches.length - 1], 10) : 0; | ||
}; | ||
const sortedTokens = globalColorTheme.tokens.sort( | ||
(a, b) => getLastNumber(a.name) - getLastNumber(b.name), | ||
); | ||
|
||
for (const token of sortedTokens) { | ||
/* Color values can only be defined by strings */ | ||
if (typeof token.value !== "string") { | ||
throw new Error(`Token value is not a string: ${token}`); | ||
} | ||
|
||
const variable = | ||
this.Figma.getVariable(token.name, collection.id) ?? | ||
this.Figma.createVariable(token.name, collection, token.figmaType); | ||
|
||
this.Figma.setVariableValue( | ||
variable, | ||
figma.util.rgba(token.value), | ||
collection.defaultModeId, | ||
); | ||
|
||
this.Figma.setVariableMetadata(variable, { | ||
codeSyntax: token.code, | ||
description: token.comment ?? "", | ||
hiddenFromPublishing: collection.hiddenFromPublishing, | ||
scopes: token.scopes, | ||
}); | ||
} | ||
|
||
console.info("Updated collection:", collection.name); | ||
} | ||
|
||
private updateScaleCollection( | ||
globalScale: | ||
| ScopedFigmaTokenConfig["radius"] | ||
| ScopedFigmaTokenConfig["spacing"], | ||
): void { | ||
const collection = | ||
this.Figma.getCollection(globalScale.name) ?? | ||
this.Figma.createCollection(globalScale.name); | ||
|
||
const sortedTokens = globalScale.tokens.sort((a, b) => { | ||
if (typeof a.value === "number" && typeof b.value === "number") { | ||
return a.value - b.value; | ||
} | ||
return 0; | ||
}); | ||
|
||
for (const token of sortedTokens) { | ||
const variable = | ||
this.Figma.getVariable(token.name, collection.id) ?? | ||
this.Figma.createVariable(token.name, collection, token.figmaType); | ||
|
||
this.Figma.setVariableValue( | ||
variable, | ||
token.value, | ||
collection.defaultModeId, | ||
); | ||
|
||
this.Figma.setVariableMetadata(variable, { | ||
codeSyntax: token.code, | ||
description: token.comment ?? "", | ||
hiddenFromPublishing: collection.hiddenFromPublishing, | ||
scopes: token.scopes, | ||
}); | ||
} | ||
|
||
console.info("Updated collection:", collection.name); | ||
} | ||
|
||
private updateSemanticColorCollection( | ||
colorsConfig: ScopedFigmaTokenConfig["colors"], | ||
): void { | ||
const semanticCollectionName = colorsConfig.light.semantic.name; | ||
|
||
const collection = | ||
this.Figma.getCollection(semanticCollectionName) ?? | ||
this.Figma.createCollection(semanticCollectionName); | ||
|
||
for (const colorEntry of Object.values(colorsConfig)) { | ||
const modeName = colorEntry.name; | ||
|
||
const globalCollection = this.Figma.getCollection( | ||
colorsConfig[modeName].global.name, | ||
); | ||
|
||
if (!globalCollection) { | ||
throw new Error( | ||
`Global collection not found for: ${colorsConfig[modeName].global.name}`, | ||
); | ||
} | ||
|
||
const mode = | ||
this.Figma.getModeWithName(modeName, collection) ?? | ||
this.Figma.createMode(modeName, collection); | ||
|
||
for (const token of colorsConfig[modeName].semantic.tokens) { | ||
const variable = | ||
this.Figma.getVariable(token.name, collection.id) ?? | ||
this.Figma.createVariable(token.name, collection, token.figmaType); | ||
|
||
if (!token.alias) { | ||
if (typeof token.value !== "string") { | ||
throw new Error( | ||
`Semantic tokens without alias requires value to be string: ${token}`, | ||
); | ||
} | ||
|
||
this.Figma.setVariableValue( | ||
variable, | ||
figma.util.rgba(token.value), | ||
mode.modeId, | ||
); | ||
continue; | ||
} | ||
|
||
const globalVariable = this.Figma.getVariable( | ||
token.alias, | ||
globalCollection.id, | ||
); | ||
|
||
if (!globalVariable) { | ||
throw new Error( | ||
`Global variable not found for alias: ${token.alias}`, | ||
); | ||
} | ||
|
||
this.Figma.setVariableValue( | ||
variable, | ||
this.Figma.createVariableAlias(globalVariable), | ||
mode.modeId, | ||
); | ||
|
||
this.Figma.setVariableMetadata(variable, { | ||
codeSyntax: token.code, | ||
description: token.comment ?? "", | ||
hiddenFromPublishing: collection.hiddenFromPublishing, | ||
scopes: token.scopes, | ||
}); | ||
} | ||
|
||
/* Make sure to remove "default" modes if they exist */ | ||
this.Figma.removeNonMatchingModes( | ||
Object.values(colorsConfig).map((colorConfig) => colorConfig.name), | ||
collection, | ||
); | ||
} | ||
|
||
console.info("Updated collection:", collection.name); | ||
} | ||
|
||
/** | ||
* Deletes all local collections in the Figma file. | ||
* @important Use with caution! If the current variables are not published in figma, | ||
* they will be lost forever! | ||
*/ | ||
resetVariables(): void { | ||
this.Figma.resetVariables(); | ||
} | ||
|
||
exitWithMessage(message: string): void { | ||
this.Figma.exit(message); | ||
} | ||
|
||
exit(): void { | ||
this.Figma.exit( | ||
`Finished updating variables for version ${this.meta.version}, last updated at ${this.meta.timestamp}`, | ||
); | ||
} | ||
} |
Oops, something went wrong.