Skip to content

Commit

Permalink
feat(script): add import from figma script (#1844)
Browse files Browse the repository at this point in the history
* feat(script): add import from figma script

- read in the output from the Export/Import Variables file
- allow user to select which mode to read from
- update the local theme file with the values
- allow for multi-modal function (use internal and external)
- DX improvements for this CLI of the script via new lib.s
- address PR comments, factoring, and linting issues
  • Loading branch information
booc0mtaco authored Feb 14, 2024
1 parent a93ae85 commit 9ed90e5
Show file tree
Hide file tree
Showing 9 changed files with 487 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
},
"overrides": [
{
"files": ["*.test.ts", "*.test.tsx", "**/test/**"],
"files": ["*.test.{js,ts}", "*.test.tsx", "**/test/**"],
"plugins": ["jest"],
"extends": ["plugin:jest/recommended"],
"env": {
Expand Down
30 changes: 30 additions & 0 deletions .storybook/components/Docs/Guidelines/Theming.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,36 @@ Once run, you will have a set of theme files written to the configured `dest` pa

To use, add this file to your core app root file **after** where the imported EDS's `@chanzuckerberg/eds/index.css` file is inserted.

### eds-import-from-Figma

Command to run: `npx eds-import-from-figma`

Designers can define and tweak the values of a custom theme. For this reason, we want to automate the application of those changes to the project's existing theme file.

To use this command, there are a few prerequisites:

* In Figma, install the [Export/Import Variables Plugin][export-import-plugin].

With the prerequisites set up, you can download a JSON file, containing the existing token definitions and values. Here's how:

* Open the design(s) in Figma
* Open the Resources Panel in the toolbar (or use <kbd>Shift + I</kbd>)
* Activate the "Export/Import Variables" plugin by first clicking the "Plugins" tab, then the "Export/Import Variables" option.
* Once it opens, click the "Export..." button for the Listed collections and save to your local machine
* Finally, us the above command, by passing in the file path to each collection.

Example:

`$ npx eds-import-from-figma path/to/export-file.json`

When using the script, it will read in the available Figma Modes in the file. Select the value you want (e.g., New Theme), then hit <kbd>Enter</kbd>

The script will run and apply new token values to the code in the appropriate places.

**NOTE**: currently, we only handle color tokens, and will skip any tokens storing other types of token values (number, etc.).

[export-import-plugin]: https://www.figma.com/community/plugin/1256972111705530093

## Custom Theming and Tailwind

When you have your own custom theme, you can use the tokens provided in `app-tailwind-theme.config.json` to do advanced tailwind configuration. This file contains all the tokens in JSON format, mapped to the literal values in your local theme.
Expand Down
110 changes: 110 additions & 0 deletions bin/_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,114 @@ module.exports = {
}
}
},
/**
* Determine the write path by taking the collection and variable name, and looking it up in
* the existing local theme. If there's a path in the local theme file, we can write there (using lodash/set
* or similar).
*
* @param {object} localTheme JSON file loaded, representing the data for the local theme
* @param {string} collectionName Name of the exported collection
* @param {string} variableName current variable name from figma (e.g., color/text/neutral/default)
* @returns {string|null} representation of the path to write to in the local theme JSON file
*/
getWritePath: function (localTheme, collectionName, variableName) {
const at = require('lodash/at');

const workingPath =
module.exports.getTokenPrefix(collectionName) +
module.exports.tokenNameToPath(variableName);

const found = at(localTheme, workingPath).filter(
(entries) => typeof entries !== 'undefined',
);

if (found.length) {
// handle case where we should look for @ in the file, then pluck the value object properly
if (found[0]['@']?.value) {
// update the write path to mark the @ and value
return workingPath + '[email protected]';
}

// handle case where it's just value
if (found[0]?.value) {
// update the write path to mark the value
return workingPath + '.value';
}
}

// There is no write path based on what's in the local theme so we return null signal it's a missing token
return null;
},
/**
* Utilities for handling parsing of Figma Theme Data.
*
* These functions are set up to handle, transform, and read data coming from figma API Structure.
* For more information on this API format, refer to:
* - https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma
* - https://www.figma.com/plugin-docs/api/VariableCollection/
*/
/**
* Given the value sourced from figma, translate it into a
* style-dictionary format. When encountering an unrecognized type,
* throw an error.
*
* Data Types:
* - Type COLOR: https://www.figma.com/plugin-docs/api/RGB/
*
* @param {string} type Figma type for the token value (Set:)
* @param {object} figmaResolvedValue
* @returns {string} value using the type
* @throws {TypeError} using `details` on the data read from figma
*/
parseResolvedValue: function (type, figmaResolvedValue) {
if (type === 'COLOR') {
const r = Math.floor(figmaResolvedValue.r * 255);
const g = Math.floor(figmaResolvedValue.g * 255);
const b = Math.floor(figmaResolvedValue.b * 255);
const a = figmaResolvedValue.a;
if (figmaResolvedValue.a > 0 && figmaResolvedValue.a < 1) {
return `rgba(${r}, ${g}, ${b}, ${a})`;
} else {
// print hex instead
return (
'#' +
[r, g, b]
.map((x) => x.toString(16))
.map((x) => (x.length === 1 ? '0' + x : x))
.join('')
.toUpperCase()
);
}
} else {
throw new TypeError('unknown resolved type: ' + type, {
details: figmaResolvedValue,
});
}
},
/**
* Given the "type" of import file (named after the collection name), produce
* a prefix to the token name that corresponds to the prefix used for those
* tokens.
*
* @param {string} collectionName The key to write to
* @returns {string|null} a text prefix for where to write the token value or null when no prefix is found
*/
getTokenPrefix: function (collectionName) {
switch (collectionName) {
case 'themes':
return 'eds.theme.';
case 'primitives':
return 'eds.';
default:
return null;
}
},
/**
* Conversion of the figma token name (e.g., some/path/to/token) to the equivalent path in a JSON object
* @param {string} figmaTokenName The name from the figma variables panel (slash separated)
* @returns {string} a lodash-compatible string representing the path to the token value in JSON
*/
tokenNameToPath: function (figmaTokenName) {
return figmaTokenName.replaceAll('/', '.').toLowerCase();
},
};
130 changes: 130 additions & 0 deletions bin/_util.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const utils = require('./_util');

describe('utils', function () {
describe('getWritePath', function () {
const exampleTheme = {
eds: {
theme: {
color: {
body: {
background: {
neutral: {
default: {
'@': {
value: '#FFFFFF',
},
hover: {
value: '#F4F6F8',
},
},
},
},
},
},
},
},
};

it('generates a write path for a root-at value', () => {
expect(
utils.getWritePath(
exampleTheme,
'themes',
'color/body/background/neutral/default',
),
).toEqual('[email protected]');
});

it('generates a write path for a non-root-at value', () => {
expect(
utils.getWritePath(
exampleTheme,
'themes',
'color/body/background/neutral/default/hover',
),
).toEqual('eds.theme.color.body.background.neutral.default.hover.value');
});

it('generates a null when the path is not in the local theme', () => {
expect(
utils.getWritePath(
exampleTheme,
'themes',
'color/body/background/neutral/default/focus',
),
).toEqual(null);
});
});

describe('getTokenPath', function () {
it.each([
['themes', 'eds.theme.'],
['primitives', 'eds.'],
['some-unknown', null],
['', null],
])(
'parses the collection %s to token prefix "%s"',
(collection, expected) => {
expect(utils.getTokenPrefix(collection)).toEqual(expected);
},
);
});

describe('tokenNameToPath', function () {
it('properly converts a token name to a lodash-compatible path', function () {
expect(utils.tokenNameToPath('some/path/to/token')).toEqual(
'some.path.to.token',
);
});
});

describe('parseResolvedValue', function () {
const space500 = {
r: 0.12941177189350128,
g: 0.1568627506494522,
b: 0.4000000059604645,
a: 1,
};

const blueprint300 = {
r: 0.027450980618596077,
g: 0.30588236451148987,
b: 0.9098039269447327,
a: 1,
};

const neutral800 = {
r: 0.12941177189350128,
g: 0.15294118225574493,
b: 0.1764705926179886,
a: 1,
};

const backgroundVeil = {
r: 0.12941177189350128,
g: 0.15294118225574493,
b: 0.1764705926179886,
a: 0.5,
};

it.each`
figmaColor | type | expected
${space500} | ${'COLOR'} | ${'#212866'}
${blueprint300} | ${'COLOR'} | ${'#074EE8'}
${neutral800} | ${'COLOR'} | ${'#21272D'}
${backgroundVeil} | ${'COLOR'} | ${'rgba(33, 39, 45, 0.5)'}
`(
'will use type $type to convert $figmaColor to $expected',
({ figmaColor, type, expected }) => {
expect(utils.parseResolvedValue(type, figmaColor)).toEqual(expected);
},
);

it('will throw on unrecognized types', () => {
const test = () => {
utils.parseResolvedValue('FLOAT', 0.5);
};
expect(test).toThrow(TypeError);
});
});
});
Loading

0 comments on commit 9ed90e5

Please sign in to comment.