Skip to content

Commit

Permalink
[Darkside] Figma plugin for updating variables (#3191)
Browse files Browse the repository at this point in the history
* 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
KenAJoh and HalvorHaugan authored Oct 4, 2024
1 parent 87aeb94 commit 678fa66
Show file tree
Hide file tree
Showing 12 changed files with 773 additions and 25 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,6 @@ module.exports = {
"**/codemod/**/*.js",
"!.storybook",
"**/playwright-report/**",
"**/tokens/**/plugin.js",
],
};
3 changes: 2 additions & 1 deletion @navikt/core/tokens/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
figma-config.json
figma-config.json
plugin.js
2 changes: 1 addition & 1 deletion @navikt/core/tokens/darkside/figma/create-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function prepareToken(
comment: token.comment,
group: token.group,
code: {
web: `var(${cssVariable.trim().split(": ")[0]})`,
WEB: `var(${cssVariable.trim().split(": ")[0]})`,
},
...figmaSettings(token),
};
Expand Down
16 changes: 9 additions & 7 deletions @navikt/core/tokens/darkside/figma/figma-config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import { StyleDictionaryToken, TokenTypes } from "../util";
export type FigmaToken = Omit<StyleDictionaryToken<TokenTypes>, "value"> & {
name: string;
alias?: string;
code: {
web: string;
};
code: Variable["codeSyntax"];
value: string | number;
figmaType: VariableResolvedDataType;
scopes: VariableScope[];
};

type FigmaConfigEntry = {
export type FigmaConfigEntry = {
name: string;
hideFromPublishing: boolean;
tokens: FigmaToken[];
Expand All @@ -20,9 +18,13 @@ type FigmaConfigEntry = {
export type FigmaTokenConfig = {
version: string;
timestamp: string;
globalLight: FigmaConfigEntry;
globalDark: FigmaConfigEntry;
semanticColors: FigmaConfigEntry;
colors: { light: ColorThemeEntry; dark: ColorThemeEntry };
radius: FigmaConfigEntry;
spacing: FigmaConfigEntry;
};

type ColorThemeEntry = {
name: "light" | "dark";
global: FigmaConfigEntry;
semantic: FigmaConfigEntry;
};
42 changes: 28 additions & 14 deletions @navikt/core/tokens/darkside/figma/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,34 @@ async function buildFigmaConfig() {
timestamp: new Date().toLocaleString("no-NO", {
timeZone: "Europe/Oslo",
}),
globalLight: {
name: "Global colors light",
hideFromPublishing: true,
tokens: lightTokens.filter(isGlobalColor),
},
globalDark: {
name: "Global colors dark",
hideFromPublishing: true,
tokens: darkTokens.filter(isGlobalColor),
},
semanticColors: {
name: "Semantic colors",
hideFromPublishing: false,
tokens: lightTokens.filter(isSemanticColor),

colors: {
light: {
name: "light",
global: {
name: "Global colors light",
hideFromPublishing: true,
tokens: lightTokens.filter(isGlobalColor),
},
semantic: {
name: "Semantic colors",
hideFromPublishing: false,
tokens: lightTokens.filter(isSemanticColor),
},
},
dark: {
name: "dark",
global: {
name: "Global colors dark",
hideFromPublishing: true,
tokens: darkTokens.filter(isGlobalColor),
},
semantic: {
name: "Semantic colors",
hideFromPublishing: false,
tokens: darkTokens.filter(isSemanticColor),
},
},
},
radius: {
name: "Radius",
Expand Down
234 changes: 234 additions & 0 deletions @navikt/core/tokens/darkside/figma/plugin/AkselVariablesInterface.ts
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}`,
);
}
}
Loading

0 comments on commit 678fa66

Please sign in to comment.