@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,430 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import { colorApproximatelyEqual, parseColor } from './color';
|
|
5
|
+
import { areSetsEqual } from './utils';
|
|
6
|
+
import { Token, TokenOrTokenGroup, TokensFile } from './token_types';
|
|
7
|
+
import {
|
|
8
|
+
GetLocalVariablesResponse,
|
|
9
|
+
LocalVariable,
|
|
10
|
+
LocalVariableCollection,
|
|
11
|
+
PostVariablesRequestBody,
|
|
12
|
+
VariableCodeSyntax,
|
|
13
|
+
VariableCreate,
|
|
14
|
+
VariableUpdate,
|
|
15
|
+
VariableValue,
|
|
16
|
+
} from '@figma/rest-api-spec';
|
|
17
|
+
|
|
18
|
+
export type FlattenedTokensByFile = {
|
|
19
|
+
[fileName: string]: {
|
|
20
|
+
[tokenName: string]: Token;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function traverseCollection({
|
|
25
|
+
key,
|
|
26
|
+
object,
|
|
27
|
+
tokens,
|
|
28
|
+
}: {
|
|
29
|
+
key: string;
|
|
30
|
+
object: TokenOrTokenGroup;
|
|
31
|
+
tokens: { [tokenName: string]: Token };
|
|
32
|
+
}) {
|
|
33
|
+
// if key is a meta field, move on
|
|
34
|
+
if (key.charAt(0) === '$') {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (object.$value !== undefined) {
|
|
39
|
+
tokens[key] = object;
|
|
40
|
+
} else {
|
|
41
|
+
Object.entries<TokenOrTokenGroup>(object).forEach(([key2, object2]) => {
|
|
42
|
+
if (key2.charAt(0) !== '$' && typeof object2 === 'object') {
|
|
43
|
+
traverseCollection({
|
|
44
|
+
key: `${key}/${key2}`,
|
|
45
|
+
object: object2,
|
|
46
|
+
tokens,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectionAndModeFromFileName(fileName: string) {
|
|
54
|
+
const fileNameParts = fileName.split('.');
|
|
55
|
+
if (fileNameParts.length < 3) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Invalid tokens file name: ${fileName}. File names must be in the format: {collectionName}.{modeName}.json`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const [collectionName, modeName] = fileNameParts;
|
|
61
|
+
return { collectionName, modeName };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function flattenTokensFile(tokensFile: TokensFile) {
|
|
65
|
+
const flattenedTokens: { [tokenName: string]: Token } = {};
|
|
66
|
+
|
|
67
|
+
Object.entries(tokensFile).forEach(([tokenGroup, groupValues]) => {
|
|
68
|
+
traverseCollection({
|
|
69
|
+
key: tokenGroup,
|
|
70
|
+
object: groupValues,
|
|
71
|
+
tokens: flattenedTokens,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return flattenedTokens;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function readJsonFiles(files: string[]) {
|
|
79
|
+
const tokensJsonByFile: FlattenedTokensByFile = {};
|
|
80
|
+
|
|
81
|
+
const seenCollectionsAndModes = new Set<string>();
|
|
82
|
+
|
|
83
|
+
files.forEach(file => {
|
|
84
|
+
const baseFileName = path.basename(file);
|
|
85
|
+
const { collectionName, modeName } =
|
|
86
|
+
collectionAndModeFromFileName(baseFileName);
|
|
87
|
+
|
|
88
|
+
if (seenCollectionsAndModes.has(`${collectionName}.${modeName}`)) {
|
|
89
|
+
throw new Error(`Duplicate collection and mode in file: ${file}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
seenCollectionsAndModes.add(`${collectionName}.${modeName}`);
|
|
93
|
+
|
|
94
|
+
const fileContent = fs.readFileSync(file, { encoding: 'utf-8' });
|
|
95
|
+
|
|
96
|
+
if (!fileContent) {
|
|
97
|
+
throw new Error(`Invalid tokens file: ${file}. File is empty.`);
|
|
98
|
+
}
|
|
99
|
+
const tokensFile: TokensFile = JSON.parse(fileContent);
|
|
100
|
+
|
|
101
|
+
tokensJsonByFile[baseFileName] = flattenTokensFile(tokensFile);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return tokensJsonByFile;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function variableResolvedTypeFromToken(token: Token) {
|
|
108
|
+
switch (token.$type) {
|
|
109
|
+
case 'color':
|
|
110
|
+
return 'COLOR';
|
|
111
|
+
case 'number':
|
|
112
|
+
return 'FLOAT';
|
|
113
|
+
case 'string':
|
|
114
|
+
return 'STRING';
|
|
115
|
+
case 'boolean':
|
|
116
|
+
return 'BOOLEAN';
|
|
117
|
+
default:
|
|
118
|
+
throw new Error(`Invalid token $type: ${token.$type}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isAlias(value: string) {
|
|
123
|
+
return value.toString().trim().charAt(0) === '{';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function variableValueFromToken(
|
|
127
|
+
token: Token,
|
|
128
|
+
localVariablesByCollectionAndName: {
|
|
129
|
+
[variableCollectionId: string]: {
|
|
130
|
+
[variableName: string]: LocalVariable;
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
): VariableValue {
|
|
134
|
+
if (typeof token.$value === 'string' && isAlias(token.$value)) {
|
|
135
|
+
// Assume aliases are in the format {group.subgroup.token} with any number of optional groups/subgroups
|
|
136
|
+
// The Figma syntax for variable names is: group/subgroup/token
|
|
137
|
+
const value = token.$value
|
|
138
|
+
.trim()
|
|
139
|
+
.replace(/\./g, '/')
|
|
140
|
+
.replace(/[{}]/g, '');
|
|
141
|
+
|
|
142
|
+
// When mapping aliases to existing local variables, we assume that variable names
|
|
143
|
+
// are unique *across all collections* in the Figma file
|
|
144
|
+
for (const localVariablesByName of Object.values(
|
|
145
|
+
localVariablesByCollectionAndName,
|
|
146
|
+
)) {
|
|
147
|
+
if (localVariablesByName[value]) {
|
|
148
|
+
return {
|
|
149
|
+
type: 'VARIABLE_ALIAS',
|
|
150
|
+
id: localVariablesByName[value].id,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// If we don't find a local variable matching the alias, we assume it's a variable
|
|
156
|
+
// that we're going to create elsewhere in the payload.
|
|
157
|
+
// If the file has an invalid alias, we rely on the Figma API to return a 400 error
|
|
158
|
+
return {
|
|
159
|
+
type: 'VARIABLE_ALIAS',
|
|
160
|
+
id: value,
|
|
161
|
+
};
|
|
162
|
+
} else if (typeof token.$value === 'string' && token.$type === 'color') {
|
|
163
|
+
return parseColor(token.$value);
|
|
164
|
+
}
|
|
165
|
+
return token.$value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function compareVariableValues(a: VariableValue, b: VariableValue) {
|
|
169
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
170
|
+
if (
|
|
171
|
+
'type' in a &&
|
|
172
|
+
'type' in b &&
|
|
173
|
+
a.type === 'VARIABLE_ALIAS' &&
|
|
174
|
+
b.type === 'VARIABLE_ALIAS'
|
|
175
|
+
) {
|
|
176
|
+
return a.id === b.id;
|
|
177
|
+
} else if ('r' in a && 'r' in b) {
|
|
178
|
+
return colorApproximatelyEqual(a, b);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
return a === b;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isCodeSyntaxEqual(a: VariableCodeSyntax, b: VariableCodeSyntax) {
|
|
188
|
+
return (
|
|
189
|
+
Object.keys(a).length === Object.keys(b).length &&
|
|
190
|
+
Object.keys(a).every(
|
|
191
|
+
key =>
|
|
192
|
+
a[key as keyof VariableCodeSyntax] ===
|
|
193
|
+
b[key as keyof VariableCodeSyntax],
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get writable token properties that are different from the variable.
|
|
200
|
+
* If the variable does not exist, all writable properties are returned.
|
|
201
|
+
*
|
|
202
|
+
* This function is being used to decide what properties to include in the
|
|
203
|
+
* POST variables call to update variables in Figma. If a token does not have
|
|
204
|
+
* a particular property, we do not include it in the differences object to avoid
|
|
205
|
+
* touching that property in Figma.
|
|
206
|
+
*/
|
|
207
|
+
function tokenAndVariableDifferences(
|
|
208
|
+
token: Token,
|
|
209
|
+
variable: LocalVariable | null,
|
|
210
|
+
) {
|
|
211
|
+
const differences: Partial<
|
|
212
|
+
Omit<
|
|
213
|
+
VariableCreate | VariableUpdate,
|
|
214
|
+
'id' | 'name' | 'variableCollectionId' | 'resolvedType' | 'action'
|
|
215
|
+
>
|
|
216
|
+
> = {};
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
token.$description !== undefined &&
|
|
220
|
+
(!variable || token.$description !== variable.description)
|
|
221
|
+
) {
|
|
222
|
+
differences.description = token.$description;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (token.$extensions && token.$extensions['com.figma']) {
|
|
226
|
+
const figmaExtensions = token.$extensions['com.figma'];
|
|
227
|
+
|
|
228
|
+
if (
|
|
229
|
+
figmaExtensions.hiddenFromPublishing !== undefined &&
|
|
230
|
+
(!variable ||
|
|
231
|
+
figmaExtensions.hiddenFromPublishing !==
|
|
232
|
+
variable.hiddenFromPublishing)
|
|
233
|
+
) {
|
|
234
|
+
differences.hiddenFromPublishing =
|
|
235
|
+
figmaExtensions.hiddenFromPublishing;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
figmaExtensions.scopes &&
|
|
240
|
+
(!variable ||
|
|
241
|
+
!areSetsEqual(
|
|
242
|
+
new Set(figmaExtensions.scopes),
|
|
243
|
+
new Set(variable.scopes),
|
|
244
|
+
))
|
|
245
|
+
) {
|
|
246
|
+
differences.scopes = figmaExtensions.scopes;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (
|
|
250
|
+
figmaExtensions.codeSyntax &&
|
|
251
|
+
(!variable ||
|
|
252
|
+
!isCodeSyntaxEqual(
|
|
253
|
+
figmaExtensions.codeSyntax,
|
|
254
|
+
variable.codeSyntax,
|
|
255
|
+
))
|
|
256
|
+
) {
|
|
257
|
+
differences.codeSyntax = figmaExtensions.codeSyntax;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return differences;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function generatePostVariablesPayload(
|
|
265
|
+
tokensByFile: FlattenedTokensByFile,
|
|
266
|
+
localVariables: GetLocalVariablesResponse,
|
|
267
|
+
) {
|
|
268
|
+
const localVariableCollectionsByName: {
|
|
269
|
+
[name: string]: LocalVariableCollection;
|
|
270
|
+
} = {};
|
|
271
|
+
const localVariablesByCollectionAndName: {
|
|
272
|
+
[variableCollectionId: string]: {
|
|
273
|
+
[variableName: string]: LocalVariable;
|
|
274
|
+
};
|
|
275
|
+
} = {};
|
|
276
|
+
|
|
277
|
+
Object.values(localVariables.meta.variableCollections).forEach(
|
|
278
|
+
collection => {
|
|
279
|
+
if (localVariableCollectionsByName[collection.name]) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Duplicate variable collection in file: ${collection.name}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
localVariableCollectionsByName[collection.name] = collection;
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
Object.values(localVariables.meta.variables).forEach(variable => {
|
|
290
|
+
if (!localVariablesByCollectionAndName[variable.variableCollectionId]) {
|
|
291
|
+
localVariablesByCollectionAndName[variable.variableCollectionId] =
|
|
292
|
+
{};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
localVariablesByCollectionAndName[variable.variableCollectionId][
|
|
296
|
+
variable.name
|
|
297
|
+
] = variable;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
console.log(
|
|
301
|
+
'Local variable collections in Figma file:',
|
|
302
|
+
Object.keys(localVariableCollectionsByName),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const postVariablesPayload: PostVariablesRequestBody = {
|
|
306
|
+
variableCollections: [],
|
|
307
|
+
variableModes: [],
|
|
308
|
+
variables: [],
|
|
309
|
+
variableModeValues: [],
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
Object.entries(tokensByFile).forEach(([fileName, tokens]) => {
|
|
313
|
+
const { collectionName, modeName } =
|
|
314
|
+
collectionAndModeFromFileName(fileName);
|
|
315
|
+
|
|
316
|
+
const variableCollection =
|
|
317
|
+
localVariableCollectionsByName[collectionName];
|
|
318
|
+
// Use the actual variable collection id or use the name as the temporary id
|
|
319
|
+
const variableCollectionId = variableCollection
|
|
320
|
+
? variableCollection.id
|
|
321
|
+
: collectionName;
|
|
322
|
+
const variableMode = variableCollection?.modes.find(
|
|
323
|
+
mode => mode.name === modeName,
|
|
324
|
+
);
|
|
325
|
+
// Use the actual mode id or use the name as the temporary id
|
|
326
|
+
const modeId = variableMode ? variableMode.modeId : modeName;
|
|
327
|
+
|
|
328
|
+
if (
|
|
329
|
+
!variableCollection &&
|
|
330
|
+
!postVariablesPayload.variableCollections!.find(
|
|
331
|
+
c => c.id === variableCollectionId,
|
|
332
|
+
)
|
|
333
|
+
) {
|
|
334
|
+
postVariablesPayload.variableCollections!.push({
|
|
335
|
+
action: 'CREATE',
|
|
336
|
+
id: variableCollectionId,
|
|
337
|
+
name: variableCollectionId,
|
|
338
|
+
initialModeId: modeId, // Use the initial mode as the first mode
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Rename the initial mode, since we're using it as our first mode in the collection
|
|
342
|
+
postVariablesPayload.variableModes!.push({
|
|
343
|
+
action: 'UPDATE',
|
|
344
|
+
id: modeId,
|
|
345
|
+
name: modeId,
|
|
346
|
+
variableCollectionId,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Add a new mode if it doesn't exist in the Figma file
|
|
351
|
+
// and it's not the initial mode in the collection
|
|
352
|
+
if (
|
|
353
|
+
!variableMode &&
|
|
354
|
+
!postVariablesPayload.variableCollections!.find(
|
|
355
|
+
c =>
|
|
356
|
+
c.id === variableCollectionId &&
|
|
357
|
+
'initialModeId' in c &&
|
|
358
|
+
c.initialModeId === modeId,
|
|
359
|
+
)
|
|
360
|
+
) {
|
|
361
|
+
postVariablesPayload.variableModes!.push({
|
|
362
|
+
action: 'CREATE',
|
|
363
|
+
id: modeId,
|
|
364
|
+
name: modeId,
|
|
365
|
+
variableCollectionId,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const localVariablesByName =
|
|
370
|
+
localVariablesByCollectionAndName[variableCollection?.id] || {};
|
|
371
|
+
|
|
372
|
+
Object.entries(tokens).forEach(([tokenName, token]) => {
|
|
373
|
+
const variable = localVariablesByName[tokenName];
|
|
374
|
+
const variableId = variable ? variable.id : tokenName;
|
|
375
|
+
const variableInPayload = postVariablesPayload.variables!.find(
|
|
376
|
+
v =>
|
|
377
|
+
v.id === variableId &&
|
|
378
|
+
'variableCollectionId' in v &&
|
|
379
|
+
v.variableCollectionId === variableCollectionId,
|
|
380
|
+
);
|
|
381
|
+
const differences = tokenAndVariableDifferences(token, variable);
|
|
382
|
+
|
|
383
|
+
// Add a new variable if it doesn't exist in the Figma file,
|
|
384
|
+
// and we haven't added it already in another mode
|
|
385
|
+
if (!variable && !variableInPayload) {
|
|
386
|
+
postVariablesPayload.variables!.push({
|
|
387
|
+
action: 'CREATE',
|
|
388
|
+
id: variableId,
|
|
389
|
+
name: tokenName,
|
|
390
|
+
variableCollectionId,
|
|
391
|
+
resolvedType: variableResolvedTypeFromToken(token),
|
|
392
|
+
...differences,
|
|
393
|
+
});
|
|
394
|
+
} else if (variable && Object.keys(differences).length > 0) {
|
|
395
|
+
if (variable.remote) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Cannot update remote variable "${variable.name}" in collection "${collectionName}"`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
postVariablesPayload.variables!.push({
|
|
402
|
+
action: 'UPDATE',
|
|
403
|
+
id: variableId,
|
|
404
|
+
...differences,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const existingVariableValue =
|
|
409
|
+
variable && variableMode ? variable.valuesByMode[modeId] : null;
|
|
410
|
+
const newVariableValue = variableValueFromToken(
|
|
411
|
+
token,
|
|
412
|
+
localVariablesByCollectionAndName,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Only include the variable mode value in the payload if it's different from the existing value
|
|
416
|
+
if (
|
|
417
|
+
existingVariableValue === null ||
|
|
418
|
+
!compareVariableValues(existingVariableValue, newVariableValue)
|
|
419
|
+
) {
|
|
420
|
+
postVariablesPayload.variableModeValues!.push({
|
|
421
|
+
variableId,
|
|
422
|
+
modeId,
|
|
423
|
+
value: newVariableValue,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return postVariablesPayload;
|
|
430
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file defines what design tokens and design token files look like in the codebase.
|
|
3
|
+
*
|
|
4
|
+
* Tokens are distinct from variables, in that a [token](https://tr.designtokens.org/format/#design-token)
|
|
5
|
+
* is a name/value pair (with other properties), while a variable in Figma stores multiple values,
|
|
6
|
+
* one for each mode.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { VariableCodeSyntax, VariableScope } from '@figma/rest-api-spec';
|
|
10
|
+
|
|
11
|
+
export interface Token {
|
|
12
|
+
/**
|
|
13
|
+
* The [type](https://tr.designtokens.org/format/#type-0) of the token.
|
|
14
|
+
*
|
|
15
|
+
* We allow `string` and `boolean` types in addition to the draft W3C spec's `color` and `number` types
|
|
16
|
+
* to align with the resolved types for Figma variables.
|
|
17
|
+
*/
|
|
18
|
+
$type: 'color' | 'number' | 'string' | 'boolean';
|
|
19
|
+
$value: string | number | boolean;
|
|
20
|
+
$description?: string;
|
|
21
|
+
$extensions?: {
|
|
22
|
+
/**
|
|
23
|
+
* The `com.figma` namespace stores Figma-specific variable properties
|
|
24
|
+
*/
|
|
25
|
+
'com.figma'?: {
|
|
26
|
+
hiddenFromPublishing?: boolean;
|
|
27
|
+
scopes?: VariableScope[];
|
|
28
|
+
codeSyntax?: VariableCodeSyntax;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type TokenOrTokenGroup =
|
|
34
|
+
| Token
|
|
35
|
+
| ({
|
|
36
|
+
[tokenName: string]: Token;
|
|
37
|
+
} & { $type?: never; $value?: never });
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Defines what we expect a Design Tokens file to look like in the codebase.
|
|
41
|
+
*
|
|
42
|
+
* This format mostly adheres to the [draft W3C spec for Design Tokens](https://tr.designtokens.org/format/)
|
|
43
|
+
* as of its most recent 24 July 2023 revision except for the $type property, for which
|
|
44
|
+
* we allow `string` and `boolean` values in addition to the spec's `color` and `number` values.
|
|
45
|
+
* We need to support `string` and `boolean` types to align with the resolved types for Figma variables.
|
|
46
|
+
*
|
|
47
|
+
* Additionally, we expect each tokens file to define tokens for a single variable collection and mode.
|
|
48
|
+
* There currently isn't a way to represent modes or themes in the W3C community group design token specification.
|
|
49
|
+
* Once the spec resolves how it wants to treat/handle modes, this code will be updated to reflect the new standard.
|
|
50
|
+
*
|
|
51
|
+
* Follow this discussion for updates: https://github.com/design-tokens/community-group/issues/210
|
|
52
|
+
*/
|
|
53
|
+
export type TokensFile = {
|
|
54
|
+
[key: string]: TokenOrTokenGroup;
|
|
55
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function green(msg: string) {
|
|
2
|
+
return `\x1b[32m${msg}\x1b[0m`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function brightRed(msg: string) {
|
|
6
|
+
return `\x1b[1;31m${msg}\x1b[0m`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function areSetsEqual<T>(a: Set<T>, b: Set<T>) {
|
|
10
|
+
return a.size === b.size && Array.from(a).every(item => b.has(item));
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const postcss = require('postcss');
|
|
2
|
+
const Case = require('case');
|
|
3
|
+
|
|
4
|
+
const genPropName = prop => Case.camel(prop.replace(/^--(ffe-)?/, ''));
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract all custom properties from a given stylesheet and create an object
|
|
8
|
+
* mapping properties to thier value.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = async css => {
|
|
12
|
+
const { root } = await postcss().process(css, {
|
|
13
|
+
from: false,
|
|
14
|
+
hideNothingWarning: true,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const obj = {};
|
|
18
|
+
|
|
19
|
+
root.walkDecls(decl => {
|
|
20
|
+
obj[genPropName(decl.prop)] = decl.value;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return obj;
|
|
24
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const less = require('less');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Takes less variables from a given file and generates a css stylesheet with
|
|
6
|
+
* these variables as custom properties on the :root pseudo-class.
|
|
7
|
+
*/
|
|
8
|
+
module.exports = async inputFile => {
|
|
9
|
+
const data = await fs.readFile(inputFile, 'utf8');
|
|
10
|
+
const root = await less.parse(data, { filename: inputFile });
|
|
11
|
+
|
|
12
|
+
const propertyNames = root.rules
|
|
13
|
+
.filter(r => r.variable)
|
|
14
|
+
.map(n => n.name.slice(1));
|
|
15
|
+
|
|
16
|
+
const { css } = await less.render(
|
|
17
|
+
`@import '${inputFile}';` +
|
|
18
|
+
`@props: ${propertyNames.join(', ')};` +
|
|
19
|
+
`:root, :host { each(@props, { --@{value}: @@value; }); }`,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return css;
|
|
23
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = filename => data =>
|
|
5
|
+
fs
|
|
6
|
+
.mkdir(path.dirname(filename), { recursive: true })
|
|
7
|
+
.then(() => fs.writeFile(filename, data, 'utf8'))
|
|
8
|
+
.then(() => console.log(`wrote ${filename}`))
|
|
9
|
+
.then(() => data); // return input data for smoother pipelines
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
function getProcessArguments() {
|
|
7
|
+
const args = {};
|
|
8
|
+
console.log(process.argv);
|
|
9
|
+
process.argv.slice(2).forEach((arg) => {
|
|
10
|
+
if (!arg.includes("=")) return;
|
|
11
|
+
const [key, value] = arg.split("=");
|
|
12
|
+
args[key] = value.split(",");
|
|
13
|
+
});
|
|
14
|
+
return args;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const writeToFile = (filename) => (data) => {
|
|
18
|
+
const dir = path.dirname(filename);
|
|
19
|
+
if (!fs.existsSync(dir)) {
|
|
20
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
fs.writeFileSync(filename, data, "utf8");
|
|
23
|
+
console.log(`wrote ${filename}`);
|
|
24
|
+
return data; // return input data for smoother pipelines
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
module.exports = { getProcessArguments, writeToFile };
|