@sb1/ffe-core 100.0.0 → 100.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/css/ffe.css +0 -1
- package/less/dimensions.less +1 -0
- package/less/variables.less +0 -1
- package/package.json +6 -4
- package/scripts/build-colors/build-colors.cjs +115 -0
- package/scripts/build-colors/json/json.cjs +12 -0
- package/scripts/build-colors/mobile/android.cjs +99 -0
- package/scripts/build-colors/mobile/android.test.js +98 -0
- package/scripts/build-colors/mobile/ios.cjs +205 -0
- package/scripts/build-colors/web/chevron.cjs +44 -0
- package/scripts/build-colors/web/color-names.cjs +24 -0
- package/scripts/build-colors/web/css.cjs +78 -0
- package/scripts/build-colors/web/js.cjs +44 -0
- package/scripts/build-colors/web/tailwind.cjs +36 -0
- package/scripts/build-colors/web/tailwind4.cjs +24 -0
- package/scripts/build-colors/web/web.cjs +18 -0
- package/scripts/build-custom-mq.cjs +45 -0
- package/scripts/build.cjs +38 -0
- package/scripts/figma-api/color.spec.tsx +126 -0
- package/scripts/figma-api/color.ts +52 -0
- package/scripts/figma-api/figma_api.ts +41 -0
- package/scripts/figma-api/sync_figma_to_tokens.ts +57 -0
- package/scripts/figma-api/token_export.spec.tsx +359 -0
- package/scripts/figma-api/token_export.ts +99 -0
- package/scripts/figma-api/token_import.spec.tsx +1097 -0
- package/scripts/figma-api/token_import.ts +430 -0
- package/scripts/figma-api/token_types.ts +55 -0
- package/scripts/figma-api/utils.ts +11 -0
- package/scripts/lib/extractCustomProps.cjs +24 -0
- package/scripts/lib/genTypeScript.cjs +7 -0
- package/scripts/lib/renderLessVarsToCSSProps.cjs +23 -0
- package/scripts/lib/writeToFile.cjs +9 -0
- package/scripts/utils.cjs +27 -0
- package/tokens/01 Primitive.value.json +4726 -0
- package/tokens/02 Semantic.Dark.json +3942 -0
- package/tokens/02 Semantic.Light.json +3942 -0
- package/tokens/03 Context.Accent.json +2273 -0
- package/tokens/03 Context.Default.json +2273 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { writeToFile } = require('../../utils.cjs');
|
|
3
|
+
|
|
4
|
+
function transformToCssProperty(name, value) {
|
|
5
|
+
return `--ffe-${name.replaceAll('.', '-')}: ${value};`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function generateCssColorFileContent(colors) {
|
|
9
|
+
return `
|
|
10
|
+
/* Generated from Figma tokens */
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/* dark mode default context */
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
.regard-color-scheme-preference,
|
|
17
|
+
.regard-color-scheme-preference .ffe-default-mode {
|
|
18
|
+
${Object.entries(colors.dark)
|
|
19
|
+
.map(([name, value]) => transformToCssProperty(name, value))
|
|
20
|
+
.join('\n')}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/* dark mode accent når en overstyrer os/nettleser light/dark preferanse*/
|
|
26
|
+
.ffe-dark,
|
|
27
|
+
.ffe-dark .ffe-default-mode {
|
|
28
|
+
${Object.entries(colors.dark)
|
|
29
|
+
.map(([name, value]) => transformToCssProperty(name, value))
|
|
30
|
+
.join('\n')}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* dark mode accent context */
|
|
34
|
+
@media (prefers-color-scheme: dark) {
|
|
35
|
+
.regard-color-scheme-preference .ffe-accent-mode {
|
|
36
|
+
${Object.entries(colors.darkAccent)
|
|
37
|
+
.map(([name, value]) => transformToCssProperty(name, value))
|
|
38
|
+
.join('\n')}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* dark mode accent når en overstyrer os/nettleser light/dark preferanser*/
|
|
43
|
+
.ffe-dark .ffe-accent-mode, .ffe-dark.ffe-accent-mode {
|
|
44
|
+
${Object.entries(colors.darkAccent)
|
|
45
|
+
.map(([name, value]) => transformToCssProperty(name, value))
|
|
46
|
+
.join('\n')}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Default context */
|
|
50
|
+
:root,
|
|
51
|
+
:host,
|
|
52
|
+
.ffe-default-mode,
|
|
53
|
+
.ffe-light,
|
|
54
|
+
.ffe-light .ffe-default-mode, .ffe-light.ffe-default-mode {
|
|
55
|
+
${Object.entries(colors.light)
|
|
56
|
+
.map(([name, value]) => transformToCssProperty(name, value))
|
|
57
|
+
.join('\n')}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Accent context */
|
|
61
|
+
.ffe-light .ffe-accent-mode,
|
|
62
|
+
.ffe-accent-mode {
|
|
63
|
+
${Object.entries(colors.lightAccent)
|
|
64
|
+
.map(([name, value]) => transformToCssProperty(name, value))
|
|
65
|
+
.join('\n')}
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildCssColors(paths, colors) {
|
|
71
|
+
paths.forEach(path => {
|
|
72
|
+
const content = generateCssColorFileContent(colors);
|
|
73
|
+
writeToFile(path + 'less/colors-semantic.less')(content);
|
|
74
|
+
writeToFile(path + 'css/colors-semantic.css')(content);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { buildCssColors };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { writeToFile } = require('../../utils.cjs');
|
|
3
|
+
|
|
4
|
+
function transformToCssPropertyName(name) {
|
|
5
|
+
return `--ffe-${name.replaceAll('.', '-')}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function transformToJsVariableName(name) {
|
|
9
|
+
return name
|
|
10
|
+
.slice(6)
|
|
11
|
+
.replaceAll('-', '.')
|
|
12
|
+
.split('.')
|
|
13
|
+
.map((part, idx) =>
|
|
14
|
+
idx === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1),
|
|
15
|
+
)
|
|
16
|
+
.join('');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function generateJsColorFileContent(colors) {
|
|
20
|
+
return `
|
|
21
|
+
"use strict";
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
|
|
24
|
+
${Object.entries(colors.light)
|
|
25
|
+
.map(([name]) => `exports.${transformToJsVariableName(name)} = `)
|
|
26
|
+
.join('')} void 0
|
|
27
|
+
|
|
28
|
+
${Object.entries(colors.light)
|
|
29
|
+
.map(
|
|
30
|
+
([name]) =>
|
|
31
|
+
`exports.${transformToJsVariableName(name)} = 'var(${transformToCssPropertyName(name)})';`,
|
|
32
|
+
)
|
|
33
|
+
.join('\n')}
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildJsColors(paths, colors) {
|
|
38
|
+
paths.forEach(path => {
|
|
39
|
+
const content = generateJsColorFileContent(colors);
|
|
40
|
+
writeToFile(path + 'lib/semanticColors.js')(content);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { buildJsColors };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { writeToFile } = require('../../utils.cjs');
|
|
3
|
+
|
|
4
|
+
function transformToCssPropertyName(name) {
|
|
5
|
+
return `--ffe-${name.replaceAll('.', '-')}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function transformToTailwindJsVariableName(name) {
|
|
9
|
+
return `['${name.slice(6).replaceAll('.', '-')}']`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function generateJsTailwindColorFileContent(colors) {
|
|
13
|
+
return `"use strict";
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
|
|
16
|
+
${Object.entries(colors.light)
|
|
17
|
+
.map(([name]) => `exports${transformToTailwindJsVariableName(name)} = `)
|
|
18
|
+
.join('')} void 0
|
|
19
|
+
|
|
20
|
+
${Object.entries(colors.light)
|
|
21
|
+
.map(
|
|
22
|
+
([name]) =>
|
|
23
|
+
`exports${transformToTailwindJsVariableName(name)} = 'var(${transformToCssPropertyName(name)})';`,
|
|
24
|
+
)
|
|
25
|
+
.join('\n')}
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildTailwindJsColors(paths, colors) {
|
|
30
|
+
paths.forEach(path => {
|
|
31
|
+
const content = generateJsTailwindColorFileContent(colors);
|
|
32
|
+
writeToFile(path + 'lib/semanticColorsTailwind.js')(content);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { buildTailwindJsColors };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { writeToFile } = require('../../utils.cjs');
|
|
3
|
+
|
|
4
|
+
function transformToTW4CssProperty(name, value) {
|
|
5
|
+
return `--${name.replaceAll('.', '-')}: var(--ffe-${name.replaceAll('.', '-')});`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function generateTailwind4CssColorFileContent(colors) {
|
|
9
|
+
return `@theme inline {
|
|
10
|
+
\t${Object.entries(colors.light)
|
|
11
|
+
.map(([name]) => transformToTW4CssProperty(name))
|
|
12
|
+
.join('\n\t')}
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildTailwind4CssColors(paths, colors) {
|
|
18
|
+
paths.forEach(path => {
|
|
19
|
+
const content = generateTailwind4CssColorFileContent(colors);
|
|
20
|
+
writeToFile(path + 'css/colors-semantic-tailwind-4.css')(content);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { buildTailwind4CssColors };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { buildChevronCssColors } = require('./chevron.cjs');
|
|
3
|
+
const { buildCssColors } = require('./css.cjs');
|
|
4
|
+
const { buildJsColors } = require('./js.cjs');
|
|
5
|
+
const { buildColorsNames } = require('./color-names.cjs');
|
|
6
|
+
const { buildTailwindJsColors } = require('./tailwind.cjs');
|
|
7
|
+
const { buildTailwind4CssColors } = require('./tailwind4.cjs');
|
|
8
|
+
|
|
9
|
+
function buildWebColors(paths, colors) {
|
|
10
|
+
buildCssColors(paths, colors);
|
|
11
|
+
buildJsColors(paths, colors);
|
|
12
|
+
buildChevronCssColors(paths, colors);
|
|
13
|
+
buildColorsNames(paths, colors);
|
|
14
|
+
buildTailwindJsColors(paths, colors);
|
|
15
|
+
buildTailwind4CssColors(paths, colors);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { buildWebColors };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const postcss = require('postcss');
|
|
4
|
+
|
|
5
|
+
const renderLessVarsToCSSProps = require('./lib/renderLessVarsToCSSProps.cjs');
|
|
6
|
+
const writeToFile = require('./lib/writeToFile.cjs');
|
|
7
|
+
|
|
8
|
+
const lessFile = process.argv[2];
|
|
9
|
+
const customMqFile = process.argv[3];
|
|
10
|
+
|
|
11
|
+
if (!customMqFile) {
|
|
12
|
+
console.error('Usage: build-custom-mq.js input.less output.css');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* convert css custom props. to css custom media queries
|
|
18
|
+
*
|
|
19
|
+
* input: @breakpoint-sm: 480px;
|
|
20
|
+
* result: @custom-media --ffe-breakpoint-sm screen and (min-width: 480px);
|
|
21
|
+
*/
|
|
22
|
+
renderLessVarsToCSSProps(path.resolve(lessFile))
|
|
23
|
+
.then(css =>
|
|
24
|
+
postcss().process(css, {
|
|
25
|
+
from: false,
|
|
26
|
+
hideNothingWarning: true,
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
.then(({ root }) => {
|
|
30
|
+
let str = '';
|
|
31
|
+
|
|
32
|
+
root.walkDecls(decl => {
|
|
33
|
+
str += `@custom-media ${decl.prop.replace(
|
|
34
|
+
/^--(ffe-)?/,
|
|
35
|
+
'--ffe-',
|
|
36
|
+
)} screen and (min-width: ${decl.value});\n`;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return str;
|
|
40
|
+
})
|
|
41
|
+
.then(writeToFile(customMqFile))
|
|
42
|
+
.catch(err => {
|
|
43
|
+
console.error(err);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const writeToFile = require('./lib/writeToFile.cjs');
|
|
5
|
+
const renderLessVarsToCSSProps = require('./lib/renderLessVarsToCSSProps.cjs');
|
|
6
|
+
const extractCustomProps = require('./lib/extractCustomProps.cjs');
|
|
7
|
+
const { genTSSource, genTSModIndex } = require('./lib/genTypeScript.cjs');
|
|
8
|
+
|
|
9
|
+
const configFilePath = process.argv[2];
|
|
10
|
+
|
|
11
|
+
if (!configFilePath) {
|
|
12
|
+
console.error('Usage: build.js path/to/build.config.js');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const config = require(path.resolve(configFilePath));
|
|
17
|
+
const lessFiles = config.sources.map(p => path.resolve(p));
|
|
18
|
+
|
|
19
|
+
const basename = fname => path.basename(fname, '.less');
|
|
20
|
+
const cssFilePathFor = fname =>
|
|
21
|
+
path.join(config.output.css, `${basename(fname)}.css`);
|
|
22
|
+
const tsFilePathFor = fname =>
|
|
23
|
+
path.join(config.output.typescript, `${basename(fname)}.ts`);
|
|
24
|
+
|
|
25
|
+
const pipeline = lessFile =>
|
|
26
|
+
renderLessVarsToCSSProps(lessFile)
|
|
27
|
+
.then(writeToFile(cssFilePathFor(lessFile)))
|
|
28
|
+
.then(extractCustomProps)
|
|
29
|
+
.then(genTSSource)
|
|
30
|
+
.then(writeToFile(tsFilePathFor(lessFile)));
|
|
31
|
+
|
|
32
|
+
Promise.all(lessFiles.map(pipeline))
|
|
33
|
+
.then(() => genTSModIndex(lessFiles.map(basename)))
|
|
34
|
+
.then(writeToFile(path.join(config.output.typescript, 'index.ts')))
|
|
35
|
+
.catch(err => {
|
|
36
|
+
console.error(err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { colorApproximatelyEqual, parseColor, rgbToHex } from './color';
|
|
2
|
+
|
|
3
|
+
describe('colorApproximatelyEqual', () => {
|
|
4
|
+
it('compares by hex value', () => {
|
|
5
|
+
expect(
|
|
6
|
+
colorApproximatelyEqual({ r: 0, g: 0, b: 0 }, { r: 0, g: 0, b: 0 }),
|
|
7
|
+
).toBe(true);
|
|
8
|
+
expect(
|
|
9
|
+
colorApproximatelyEqual(
|
|
10
|
+
{ r: 0, g: 0, b: 0 },
|
|
11
|
+
{ r: 0, g: 0, b: 0, a: 1 },
|
|
12
|
+
),
|
|
13
|
+
).toBe(true);
|
|
14
|
+
expect(
|
|
15
|
+
colorApproximatelyEqual(
|
|
16
|
+
{ r: 0, g: 0, b: 0, a: 0.5 },
|
|
17
|
+
{ r: 0, g: 0, b: 0, a: 0.5 },
|
|
18
|
+
),
|
|
19
|
+
).toBe(true);
|
|
20
|
+
expect(
|
|
21
|
+
colorApproximatelyEqual(
|
|
22
|
+
{ r: 0, g: 0, b: 0 },
|
|
23
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
24
|
+
),
|
|
25
|
+
).toBe(false);
|
|
26
|
+
|
|
27
|
+
expect(
|
|
28
|
+
colorApproximatelyEqual(
|
|
29
|
+
{ r: 0, g: 0, b: 0 },
|
|
30
|
+
{ r: 0.001, g: 0, b: 0 },
|
|
31
|
+
),
|
|
32
|
+
).toBe(true);
|
|
33
|
+
expect(
|
|
34
|
+
colorApproximatelyEqual(
|
|
35
|
+
{ r: 0, g: 0, b: 0 },
|
|
36
|
+
{ r: 0.0028, g: 0, b: 0 },
|
|
37
|
+
),
|
|
38
|
+
).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('parseColor', () => {
|
|
43
|
+
it('parses hex values', () => {
|
|
44
|
+
// 3-value syntax
|
|
45
|
+
expect(parseColor('#000')).toEqual({ r: 0, g: 0, b: 0 });
|
|
46
|
+
expect(parseColor('#fff')).toEqual({ r: 1, g: 1, b: 1 });
|
|
47
|
+
expect(parseColor('#FFF')).toEqual({ r: 1, g: 1, b: 1 });
|
|
48
|
+
expect(parseColor('#f09')).toEqual({ r: 1, g: 0, b: 153 / 255 });
|
|
49
|
+
expect(parseColor('#F09')).toEqual({ r: 1, g: 0, b: 153 / 255 });
|
|
50
|
+
|
|
51
|
+
// 4-value syntax
|
|
52
|
+
expect(parseColor('#0000')).toEqual({ r: 0, g: 0, b: 0, a: 0 });
|
|
53
|
+
expect(parseColor('#000F')).toEqual({ r: 0, g: 0, b: 0, a: 1 });
|
|
54
|
+
expect(parseColor('#f09a')).toEqual({
|
|
55
|
+
r: 1,
|
|
56
|
+
g: 0,
|
|
57
|
+
b: 153 / 255,
|
|
58
|
+
a: 170 / 255,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 6-value syntax
|
|
62
|
+
expect(parseColor('#000000')).toEqual({ r: 0, g: 0, b: 0 });
|
|
63
|
+
expect(parseColor('#ffffff')).toEqual({ r: 1, g: 1, b: 1 });
|
|
64
|
+
expect(parseColor('#FFFFFF')).toEqual({ r: 1, g: 1, b: 1 });
|
|
65
|
+
expect(parseColor('#ff0099')).toEqual({ r: 1, g: 0, b: 153 / 255 });
|
|
66
|
+
expect(parseColor('#FF0099')).toEqual({ r: 1, g: 0, b: 153 / 255 });
|
|
67
|
+
|
|
68
|
+
// 8-value syntax
|
|
69
|
+
expect(parseColor('#00000000')).toEqual({ r: 0, g: 0, b: 0, a: 0 });
|
|
70
|
+
expect(parseColor('#00000080')).toEqual({
|
|
71
|
+
r: 0,
|
|
72
|
+
g: 0,
|
|
73
|
+
b: 0,
|
|
74
|
+
a: 128 / 255,
|
|
75
|
+
});
|
|
76
|
+
expect(parseColor('#000000ff')).toEqual({ r: 0, g: 0, b: 0, a: 1 });
|
|
77
|
+
expect(parseColor('#5EE0DCAB')).toEqual({
|
|
78
|
+
r: 0.3686274509803922,
|
|
79
|
+
g: 0.8784313725490196,
|
|
80
|
+
b: 0.8627450980392157,
|
|
81
|
+
a: 0.6705882352941176,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('handles invalid hex values', () => {
|
|
86
|
+
expect(() => parseColor('#')).toThrowError('Invalid color format');
|
|
87
|
+
expect(() => parseColor('#0')).toThrowError('Invalid color format');
|
|
88
|
+
expect(() => parseColor('#00')).toThrowError('Invalid color format');
|
|
89
|
+
expect(() => parseColor('#0000000')).toThrowError(
|
|
90
|
+
'Invalid color format',
|
|
91
|
+
);
|
|
92
|
+
expect(() => parseColor('#000000000')).toThrowError(
|
|
93
|
+
'Invalid color format',
|
|
94
|
+
);
|
|
95
|
+
expect(() => parseColor('#hhh')).toThrowError('Invalid color format');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('rgbToHex', () => {
|
|
100
|
+
it('should convert rgb to hex', () => {
|
|
101
|
+
expect(rgbToHex({ r: 1, g: 1, b: 1 })).toBe('#ffffff');
|
|
102
|
+
expect(rgbToHex({ r: 0, g: 0, b: 0 })).toBe('#000000');
|
|
103
|
+
expect(rgbToHex({ r: 0.5, g: 0.5, b: 0.5 })).toBe('#808080');
|
|
104
|
+
expect(
|
|
105
|
+
rgbToHex({
|
|
106
|
+
r: 0.3686274509803922,
|
|
107
|
+
g: 0.8784313725490196,
|
|
108
|
+
b: 0.8627450980392157,
|
|
109
|
+
}),
|
|
110
|
+
).toBe('#5ee0dc');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should convert rgba to hex', () => {
|
|
114
|
+
expect(rgbToHex({ r: 1, g: 1, b: 1, a: 1 })).toBe('#ffffff');
|
|
115
|
+
expect(rgbToHex({ r: 0, g: 0, b: 0, a: 0.5 })).toBe('#00000080');
|
|
116
|
+
expect(rgbToHex({ r: 0.5, g: 0.5, b: 0.5, a: 0.5 })).toBe('#80808080');
|
|
117
|
+
expect(
|
|
118
|
+
rgbToHex({
|
|
119
|
+
r: 0.3686274509803922,
|
|
120
|
+
g: 0.8784313725490196,
|
|
121
|
+
b: 0.8627450980392157,
|
|
122
|
+
a: 0,
|
|
123
|
+
}),
|
|
124
|
+
).toBe('#5ee0dc00');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { RGB, RGBA } from '@figma/rest-api-spec';
|
|
2
|
+
|
|
3
|
+
export function rgbToHex({ r, g, b, ...rest }: RGB | RGBA) {
|
|
4
|
+
const a = 'a' in rest ? rest.a : 1;
|
|
5
|
+
|
|
6
|
+
const toHex = (value: number) => {
|
|
7
|
+
const hex = Math.round(value * 255).toString(16);
|
|
8
|
+
return hex.length === 1 ? `0${hex}` : hex;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const hex = [toHex(r), toHex(g), toHex(b)].join('');
|
|
12
|
+
return `#${hex}${a !== 1 ? toHex(a) : ''}`.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compares two colors for approximate equality since converting between Figma RGBA objects (from 0 -> 1) and
|
|
17
|
+
* hex colors can result in slight differences.
|
|
18
|
+
*/
|
|
19
|
+
export function colorApproximatelyEqual(
|
|
20
|
+
colorA: RGB | RGBA,
|
|
21
|
+
colorB: RGB | RGBA,
|
|
22
|
+
) {
|
|
23
|
+
return rgbToHex(colorA) === rgbToHex(colorB);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseColor(color: string): RGB | RGBA {
|
|
27
|
+
const trimmedColor = color.trim();
|
|
28
|
+
const hexRegex = /^#([A-Fa-f0-9]{6})([A-Fa-f0-9]{2}){0,1}$/;
|
|
29
|
+
const hexShorthandRegex = /^#([A-Fa-f0-9]{3})([A-Fa-f0-9]){0,1}$/;
|
|
30
|
+
|
|
31
|
+
if (hexRegex.test(trimmedColor) || hexShorthandRegex.test(color)) {
|
|
32
|
+
const hexValue = trimmedColor.substring(1);
|
|
33
|
+
const expandedHex =
|
|
34
|
+
hexValue.length === 3 || hexValue.length === 4
|
|
35
|
+
? hexValue
|
|
36
|
+
.split('')
|
|
37
|
+
.map(char => char + char)
|
|
38
|
+
.join('')
|
|
39
|
+
: hexValue;
|
|
40
|
+
|
|
41
|
+
const alphaValue =
|
|
42
|
+
expandedHex.length === 8 ? expandedHex.slice(6, 8) : undefined;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
r: parseInt(expandedHex.slice(0, 2), 16) / 255,
|
|
46
|
+
g: parseInt(expandedHex.slice(2, 4), 16) / 255,
|
|
47
|
+
b: parseInt(expandedHex.slice(4, 6), 16) / 255,
|
|
48
|
+
...(alphaValue ? { a: parseInt(alphaValue, 16) / 255 } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
throw new Error('Invalid color format');
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import {
|
|
3
|
+
GetLocalVariablesResponse,
|
|
4
|
+
PostVariablesRequestBody,
|
|
5
|
+
PostVariablesResponse,
|
|
6
|
+
} from '@figma/rest-api-spec';
|
|
7
|
+
|
|
8
|
+
export default class FigmaApi {
|
|
9
|
+
private baseUrl = 'https://api.figma.com';
|
|
10
|
+
private token: string;
|
|
11
|
+
|
|
12
|
+
constructor(token: string) {
|
|
13
|
+
this.token = token;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async getLocalVariables(fileKey: string) {
|
|
17
|
+
const resp = await axios.request<GetLocalVariablesResponse>({
|
|
18
|
+
url: `${this.baseUrl}/v1/files/${fileKey}/variables/local`,
|
|
19
|
+
headers: {
|
|
20
|
+
Accept: '*/*',
|
|
21
|
+
'X-Figma-Token': this.token,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return resp.data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async postVariables(fileKey: string, payload: PostVariablesRequestBody) {
|
|
29
|
+
const resp = await axios.request<PostVariablesResponse>({
|
|
30
|
+
url: `${this.baseUrl}/v1/files/${fileKey}/variables`,
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
Accept: '*/*',
|
|
34
|
+
'X-Figma-Token': this.token,
|
|
35
|
+
},
|
|
36
|
+
data: payload,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return resp.data;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
|
|
4
|
+
import FigmaApi from './figma_api';
|
|
5
|
+
|
|
6
|
+
import { green } from './utils';
|
|
7
|
+
import { tokenFilesFromLocalVariables } from './token_export';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* // Defaults to writing to the tokens_new directory
|
|
13
|
+
* npm run sync-figma-to-tokens
|
|
14
|
+
*
|
|
15
|
+
* // Writes to the specified directory
|
|
16
|
+
* npm run sync-figma-to-tokens -- --output directory_name
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
if (!process.env.PERSONAL_ACCESS_TOKEN || !process.env.FILE_KEY) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'PERSONAL_ACCESS_TOKEN and FILE_KEY environemnt variables are required',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const fileKey = process.env.FILE_KEY;
|
|
26
|
+
|
|
27
|
+
const api = new FigmaApi(process.env.PERSONAL_ACCESS_TOKEN);
|
|
28
|
+
const localVariables = await api.getLocalVariables(fileKey);
|
|
29
|
+
|
|
30
|
+
const tokensFiles = tokenFilesFromLocalVariables(localVariables);
|
|
31
|
+
|
|
32
|
+
let outputDir = './packages/ffe-core/tokens';
|
|
33
|
+
const outputArgIdx = process.argv.indexOf('--output');
|
|
34
|
+
if (outputArgIdx !== -1) {
|
|
35
|
+
outputDir = process.argv[outputArgIdx + 1];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(outputDir)) {
|
|
39
|
+
fs.mkdirSync(outputDir);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Object.entries(tokensFiles).forEach(([fileName, fileContent]) => {
|
|
43
|
+
fs.writeFileSync(
|
|
44
|
+
`${outputDir}/${fileName}`,
|
|
45
|
+
JSON.stringify(fileContent, null, 2),
|
|
46
|
+
);
|
|
47
|
+
console.log(`Wrote ${fileName}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log(
|
|
51
|
+
green(
|
|
52
|
+
`✅ Tokens files have been written to the ${outputDir} directory`,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
main();
|