Skip to content

Commit

Permalink
feat(ffe-core): sync figma og kode, Figma Variables API
Browse files Browse the repository at this point in the history
  • Loading branch information
dagfrode committed Jan 14, 2025
1 parent d2415cb commit 8ebf408
Show file tree
Hide file tree
Showing 21 changed files with 19,034 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/ffe-core/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
css/
gen-src/
less/colors-semantic.less
1 change: 1 addition & 0 deletions packages/ffe-core/less/ffe.less
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// less variables
@import 'breakpoints';
@import 'colors';
@import 'colors-semantic';
@import 'dimensions';
@import 'motion';
@import 'spacing';
Expand Down
3 changes: 2 additions & 1 deletion packages/ffe-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"lint": "ffe-buildtool stylelint less/*.less",
"test": "npm run lint",
"clean": "rm -rf css/ gen-src/ lib/",
"build": "./scripts/build.js tokens.config.js && ./scripts/build-custom-mq.js less/breakpoints.less css/custom-media-queries.css && ffe-buildtool tsc && lessc less/ffe.less css/ffe.css --autoprefix"
"generate-semantic-colors": "./scripts/generate-semantic-colors.js",
"build": "npm run generate-semantic-colors && ./scripts/build.js tokens.config.js && ./scripts/build-custom-mq.js less/breakpoints.less css/custom-media-queries.css && ffe-buildtool tsc && lessc less/ffe.less css/ffe.css --autoprefix"
},
"devDependencies": {
"@sb1/ffe-buildtool": "^0.9.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/ffe-core/scripts/build.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#!/usr/bin/env node
const path = require('path');

const writeToFile = require('./lib/writeToFile');
const renderLessVarsToCSSProps = require('./lib/renderLessVarsToCSSProps');
const extractCustomProps = require('./lib/extractCustomProps');
const { genTSSource, genTSModIndex } = require('./lib/genTypeScript');

const configFilePath = process.argv[2];

Expand All @@ -13,7 +11,9 @@ if (!configFilePath) {
process.exit(1);
}

const { genTSSource, genTSModIndex } = require('./lib/genTypeScript');
const config = require(path.resolve(configFilePath));

const lessFiles = config.sources.map(p => path.resolve(p));

const basename = fname => path.basename(fname, '.less');
Expand Down
126 changes: 126 additions & 0 deletions packages/ffe-core/scripts/figma-api/color.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { colorApproximatelyEqual, parseColor, rgbToHex } from './color';

describe('colorApproximatelyEqual', () => {
it('compares by hex value', () => {
expect(
colorApproximatelyEqual({ r: 0, g: 0, b: 0 }, { r: 0, g: 0, b: 0 }),
).toBe(true);
expect(
colorApproximatelyEqual(
{ r: 0, g: 0, b: 0 },
{ r: 0, g: 0, b: 0, a: 1 },
),
).toBe(true);
expect(
colorApproximatelyEqual(
{ r: 0, g: 0, b: 0, a: 0.5 },
{ r: 0, g: 0, b: 0, a: 0.5 },
),
).toBe(true);
expect(
colorApproximatelyEqual(
{ r: 0, g: 0, b: 0 },
{ r: 0, g: 0, b: 0, a: 0 },
),
).toBe(false);

expect(
colorApproximatelyEqual(
{ r: 0, g: 0, b: 0 },
{ r: 0.001, g: 0, b: 0 },
),
).toBe(true);
expect(
colorApproximatelyEqual(
{ r: 0, g: 0, b: 0 },
{ r: 0.0028, g: 0, b: 0 },
),
).toBe(false);
});
});

describe('parseColor', () => {
it('parses hex values', () => {
// 3-value syntax
expect(parseColor('#000')).toEqual({ r: 0, g: 0, b: 0 });
expect(parseColor('#fff')).toEqual({ r: 1, g: 1, b: 1 });
expect(parseColor('#FFF')).toEqual({ r: 1, g: 1, b: 1 });
expect(parseColor('#f09')).toEqual({ r: 1, g: 0, b: 153 / 255 });
expect(parseColor('#F09')).toEqual({ r: 1, g: 0, b: 153 / 255 });

// 4-value syntax
expect(parseColor('#0000')).toEqual({ r: 0, g: 0, b: 0, a: 0 });
expect(parseColor('#000F')).toEqual({ r: 0, g: 0, b: 0, a: 1 });
expect(parseColor('#f09a')).toEqual({
r: 1,
g: 0,
b: 153 / 255,
a: 170 / 255,
});

// 6-value syntax
expect(parseColor('#000000')).toEqual({ r: 0, g: 0, b: 0 });
expect(parseColor('#ffffff')).toEqual({ r: 1, g: 1, b: 1 });
expect(parseColor('#FFFFFF')).toEqual({ r: 1, g: 1, b: 1 });
expect(parseColor('#ff0099')).toEqual({ r: 1, g: 0, b: 153 / 255 });
expect(parseColor('#FF0099')).toEqual({ r: 1, g: 0, b: 153 / 255 });

// 8-value syntax
expect(parseColor('#00000000')).toEqual({ r: 0, g: 0, b: 0, a: 0 });
expect(parseColor('#00000080')).toEqual({
r: 0,
g: 0,
b: 0,
a: 128 / 255,
});
expect(parseColor('#000000ff')).toEqual({ r: 0, g: 0, b: 0, a: 1 });
expect(parseColor('#5EE0DCAB')).toEqual({
r: 0.3686274509803922,
g: 0.8784313725490196,
b: 0.8627450980392157,
a: 0.6705882352941176,
});
});

it('handles invalid hex values', () => {
expect(() => parseColor('#')).toThrowError('Invalid color format');
expect(() => parseColor('#0')).toThrowError('Invalid color format');
expect(() => parseColor('#00')).toThrowError('Invalid color format');
expect(() => parseColor('#0000000')).toThrowError(
'Invalid color format',
);
expect(() => parseColor('#000000000')).toThrowError(
'Invalid color format',
);
expect(() => parseColor('#hhh')).toThrowError('Invalid color format');
});
});

describe('rgbToHex', () => {
it('should convert rgb to hex', () => {
expect(rgbToHex({ r: 1, g: 1, b: 1 })).toBe('#ffffff');
expect(rgbToHex({ r: 0, g: 0, b: 0 })).toBe('#000000');
expect(rgbToHex({ r: 0.5, g: 0.5, b: 0.5 })).toBe('#808080');
expect(
rgbToHex({
r: 0.3686274509803922,
g: 0.8784313725490196,
b: 0.8627450980392157,
}),
).toBe('#5ee0dc');
});

it('should convert rgba to hex', () => {
expect(rgbToHex({ r: 1, g: 1, b: 1, a: 1 })).toBe('#ffffff');
expect(rgbToHex({ r: 0, g: 0, b: 0, a: 0.5 })).toBe('#00000080');
expect(rgbToHex({ r: 0.5, g: 0.5, b: 0.5, a: 0.5 })).toBe('#80808080');
expect(
rgbToHex({
r: 0.3686274509803922,
g: 0.8784313725490196,
b: 0.8627450980392157,
a: 0,
}),
).toBe('#5ee0dc00');
});
});
52 changes: 52 additions & 0 deletions packages/ffe-core/scripts/figma-api/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { RGB, RGBA } from '@figma/rest-api-spec';

export function rgbToHex({ r, g, b, ...rest }: RGB | RGBA) {
const a = 'a' in rest ? rest.a : 1;

const toHex = (value: number) => {
const hex = Math.round(value * 255).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};

const hex = [toHex(r), toHex(g), toHex(b)].join('');
return `#${hex}${a !== 1 ? toHex(a) : ''}`.trim();
}

/**
* Compares two colors for approximate equality since converting between Figma RGBA objects (from 0 -> 1) and
* hex colors can result in slight differences.
*/
export function colorApproximatelyEqual(
colorA: RGB | RGBA,
colorB: RGB | RGBA,
) {
return rgbToHex(colorA) === rgbToHex(colorB);
}

export function parseColor(color: string): RGB | RGBA {
const trimmedColor = color.trim();
const hexRegex = /^#([A-Fa-f0-9]{6})([A-Fa-f0-9]{2}){0,1}$/;
const hexShorthandRegex = /^#([A-Fa-f0-9]{3})([A-Fa-f0-9]){0,1}$/;

if (hexRegex.test(trimmedColor) || hexShorthandRegex.test(color)) {
const hexValue = trimmedColor.substring(1);
const expandedHex =
hexValue.length === 3 || hexValue.length === 4
? hexValue
.split('')
.map(char => char + char)
.join('')
: hexValue;

const alphaValue =
expandedHex.length === 8 ? expandedHex.slice(6, 8) : undefined;

return {
r: parseInt(expandedHex.slice(0, 2), 16) / 255,
g: parseInt(expandedHex.slice(2, 4), 16) / 255,
b: parseInt(expandedHex.slice(4, 6), 16) / 255,
...(alphaValue ? { a: parseInt(alphaValue, 16) / 255 } : {}),
};
}
throw new Error('Invalid color format');
}
41 changes: 41 additions & 0 deletions packages/ffe-core/scripts/figma-api/figma_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import axios from 'axios';
import {
GetLocalVariablesResponse,
PostVariablesRequestBody,
PostVariablesResponse,
} from '@figma/rest-api-spec';

export default class FigmaApi {
private baseUrl = 'https://api.figma.com';
private token: string;

constructor(token: string) {
this.token = token;
}

async getLocalVariables(fileKey: string) {
const resp = await axios.request<GetLocalVariablesResponse>({
url: `${this.baseUrl}/v1/files/${fileKey}/variables/local`,
headers: {
Accept: '*/*',
'X-Figma-Token': this.token,
},
});

return resp.data;
}

async postVariables(fileKey: string, payload: PostVariablesRequestBody) {
const resp = await axios.request<PostVariablesResponse>({
url: `${this.baseUrl}/v1/files/${fileKey}/variables`,
method: 'POST',
headers: {
Accept: '*/*',
'X-Figma-Token': this.token,
},
data: payload,
});

return resp.data;
}
}
57 changes: 57 additions & 0 deletions packages/ffe-core/scripts/figma-api/sync_figma_to_tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'dotenv/config';
import * as fs from 'fs';

import FigmaApi from './figma_api';

import { green } from './utils';
import { tokenFilesFromLocalVariables } from './token_export';

/**
* Usage:
*
* // Defaults to writing to the tokens_new directory
* npm run sync-figma-to-tokens
*
* // Writes to the specified directory
* npm run sync-figma-to-tokens -- --output directory_name
*/

async function main() {
if (!process.env.PERSONAL_ACCESS_TOKEN || !process.env.FILE_KEY) {
throw new Error(
'PERSONAL_ACCESS_TOKEN and FILE_KEY environemnt variables are required',
);
}
const fileKey = process.env.FILE_KEY;

const api = new FigmaApi(process.env.PERSONAL_ACCESS_TOKEN);
const localVariables = await api.getLocalVariables(fileKey);

const tokensFiles = tokenFilesFromLocalVariables(localVariables);

let outputDir = './packages/ffe-core/tokens';
const outputArgIdx = process.argv.indexOf('--output');
if (outputArgIdx !== -1) {
outputDir = process.argv[outputArgIdx + 1];
}

if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}

Object.entries(tokensFiles).forEach(([fileName, fileContent]) => {
fs.writeFileSync(
`${outputDir}/${fileName}`,
JSON.stringify(fileContent, null, 2),
);
console.log(`Wrote ${fileName}`);
});

console.log(
green(
`✅ Tokens files have been written to the ${outputDir} directory`,
),
);
}

main();
Loading

0 comments on commit 8ebf408

Please sign in to comment.