Skip to content

Commit 1d5c60f

Browse files
fix: resolve mode-specific baseFontSize for rem scaling in variable e… (#3836)
* fix: resolve mode-specific baseFontSize for rem scaling in variable export * fix lint * update test * add changeset * update changeset
1 parent e0ad95d commit 1d5c60f

7 files changed

Lines changed: 210 additions & 10 deletions

File tree

.changeset/fresh-brooms-build.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tokens-studio/figma-plugin": patch
3+
---
4+
5+
Support per-mode `baseFontSize` resolution via aliases, ensuring correct rem-to-pixel scaling when exporting variables across different themes. Also includes improved handling of numeric/aliased font sizes in variable exports and more robust testing for dimension conversions.

packages/tokens-studio-for-figma/src/plugin/setBooleanValuesOnVariable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export default function setBooleanValuesOnVariable(variable: Variable, mode: str
44
try {
55
const existingVariableValue = variable.valuesByMode[mode];
66
if (
7-
existingVariableValue === undefined
8-
|| !(typeof existingVariableValue === 'boolean' || isVariableWithAliasReference(existingVariableValue))
7+
existingVariableValue !== undefined
8+
&& !(typeof existingVariableValue === 'boolean' || isVariableWithAliasReference(existingVariableValue))
99
) return;
1010

1111
const newValue = value === 'true';

packages/tokens-studio-for-figma/src/plugin/setColorValuesOnVariable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export default function setColorValuesOnVariable(variable: Variable, mode: strin
2323
const { color, opacity } = convertToFigmaColor(value);
2424
const existingVariableValue = variable.valuesByMode[mode];
2525
if (
26-
!existingVariableValue
27-
|| !(isFigmaColorObject(existingVariableValue) || isVariableWithAliasReference(existingVariableValue))
26+
existingVariableValue
27+
&& !(isFigmaColorObject(existingVariableValue) || isVariableWithAliasReference(existingVariableValue))
2828
) return;
2929

3030
const newValue = { ...color, a: opacity };

packages/tokens-studio-for-figma/src/plugin/setNumberValuesOnVariable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export default function setNumberValuesOnVariable(variable: Variable, mode: stri
1616
}
1717
const existingVariableValue = variable.valuesByMode[mode];
1818
if (
19-
existingVariableValue === undefined
20-
|| !(typeof existingVariableValue === 'number' || isVariableWithAliasReference(existingVariableValue))
19+
existingVariableValue !== undefined
20+
&& !(typeof existingVariableValue === 'number' || isVariableWithAliasReference(existingVariableValue))
2121
) return;
2222

2323
// For direct number values, compare using threshold

packages/tokens-studio-for-figma/src/plugin/setStringValuesOnVariable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export default function setStringValuesOnVariable(variable: Variable, mode: stri
44
try {
55
const existingVariableValue = variable.valuesByMode[mode];
66
if (
7-
!existingVariableValue
8-
|| !(typeof existingVariableValue === 'string' || isVariableWithAliasReference(existingVariableValue))
7+
existingVariableValue
8+
&& !(typeof existingVariableValue === 'string' || isVariableWithAliasReference(existingVariableValue))
99
) return;
1010

1111
if (forceUpdate || existingVariableValue !== value) {

packages/tokens-studio-for-figma/src/plugin/updateVariables.test.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,195 @@ describe('updateVariables', () => {
297297
// Verify that 2rem was converted to 30px (2 * 15)
298298
expect(mockSetValueForMode).toHaveBeenCalledWith('1:1', 30);
299299
});
300+
301+
it('should handle numeric baseFontSize results from getAliasValue (math evaluated)', async () => {
302+
mockSetValueForMode.mockClear();
303+
304+
const baselineVariable = {
305+
name: 'typography/baseline',
306+
variableCollectionId: 'VariableCollectionId:1:0',
307+
resolvedType: 'FLOAT',
308+
setValueForMode: mockSetValueForMode,
309+
id: 'VariableID:1:3',
310+
key: 'VariableID:1:3',
311+
description: '',
312+
valuesByMode: { '1:0': 16 },
313+
remote: false,
314+
remove: jest.fn(),
315+
};
316+
317+
const newVarForTest = {
318+
name: 'sizing/test',
319+
variableCollectionId: 'VariableCollectionId:1:0',
320+
resolvedType: 'FLOAT',
321+
setValueForMode: mockSetValueForMode,
322+
id: 'VariableID:1:new',
323+
key: 'VariableID:1:new',
324+
description: '',
325+
valuesByMode: { '1:0': 0 },
326+
remote: false,
327+
remove: jest.fn(),
328+
};
329+
330+
figma.variables.createVariable = jest.fn().mockImplementation((name) => {
331+
if (name === 'typography/baseline') return baselineVariable;
332+
if (name === 'sizing/test') return newVarForTest;
333+
return newVariable;
334+
});
335+
336+
figma.variables.getLocalVariables = jest.fn().mockReturnValue([]);
337+
338+
const theme = {
339+
id: 'ThemeId:test',
340+
name: 'Test',
341+
group: 'Test group',
342+
selectedTokenSets: { core: TokenSetStatus.ENABLED },
343+
};
344+
345+
const tokens = {
346+
core: [
347+
{
348+
name: 'typography.baseline',
349+
value: '16',
350+
type: TokenTypes.FONT_SIZES,
351+
},
352+
{
353+
name: 'sizing.test',
354+
value: '1.5rem',
355+
type: TokenTypes.SIZING,
356+
},
357+
],
358+
};
359+
360+
const settingsWithAlias = {
361+
...settings,
362+
aliasBaseFontSize: '{typography.baseline}',
363+
};
364+
365+
await updateVariables({
366+
collection: { id: 'VariableCollectionId:1:0' } as any,
367+
mode: '1:0',
368+
theme,
369+
tokens,
370+
settings: settingsWithAlias,
371+
overallConfig: { core: TokenSetStatus.ENABLED },
372+
});
373+
374+
// 1.5rem * 16 = 24
375+
expect(mockSetValueForMode).toHaveBeenCalledWith('1:0', 24);
376+
});
377+
378+
it('should apply different baseFontSize for different modes in the same collection', async () => {
379+
mockSetValueForMode.mockClear();
380+
const mockSpacingSetValueForMode = jest.fn();
381+
const mockBaseFontSizeSetValueForMode = jest.fn();
382+
383+
const collection: any = {
384+
id: 'collection1',
385+
modes: [
386+
{ modeId: 'mode-a', name: 'Mode A' },
387+
{ modeId: 'mode-b', name: 'Mode B' },
388+
],
389+
};
390+
391+
const tokens: any = {
392+
core: [
393+
{ name: 'base.font-size', value: '16', type: TokenTypes.NUMBER },
394+
{ name: 'spacing.small', value: '1rem', type: TokenTypes.SPACING },
395+
],
396+
dark: [
397+
{ name: 'base.font-size', value: '20', type: TokenTypes.NUMBER },
398+
],
399+
};
400+
401+
const settings: any = {
402+
aliasBaseFontSize: '{base.font-size}',
403+
variablesNumber: true,
404+
variablesColor: false,
405+
variablesString: false,
406+
variablesBoolean: false,
407+
renameExistingStylesAndVariables: false,
408+
removeStylesAndVariablesWithoutConnection: false,
409+
};
410+
411+
const themeA: any = {
412+
id: 'theme-a',
413+
name: 'Theme A',
414+
selectedTokenSets: { core: TokenSetStatus.ENABLED },
415+
$figmaModeId: 'mode-a',
416+
};
417+
418+
const themeB: any = {
419+
id: 'theme-b',
420+
name: 'Theme B',
421+
selectedTokenSets: {
422+
core: TokenSetStatus.ENABLED,
423+
dark: TokenSetStatus.ENABLED,
424+
},
425+
$figmaModeId: 'mode-b',
426+
};
427+
428+
const mockSpacingVariable: any = {
429+
id: 'var1',
430+
key: 'var1-key',
431+
name: 'spacing/small',
432+
resolvedType: 'FLOAT',
433+
variableCollectionId: 'collection1',
434+
valuesByMode: { 'mode-a': 0, 'mode-b': 0 }, // Initial values changed to 0
435+
setValueForMode: mockSpacingSetValueForMode,
436+
};
437+
438+
const mockBaseFontSizeVariable: any = {
439+
id: 'var2',
440+
key: 'var2-key',
441+
name: 'base/font-size',
442+
resolvedType: 'FLOAT',
443+
variableCollectionId: 'collection1',
444+
valuesByMode: { 'mode-a': 0, 'mode-b': 0 }, // Initial values changed to 0
445+
setValueForMode: mockBaseFontSizeSetValueForMode,
446+
};
447+
448+
(figma.variables.getLocalVariables as jest.Mock).mockReturnValue([
449+
mockSpacingVariable,
450+
mockBaseFontSizeVariable,
451+
]);
452+
(figma.variables.getVariableByIdAsync as jest.Mock).mockImplementation(async (id: string) => {
453+
if (id === mockSpacingVariable.id) return mockSpacingVariable;
454+
if (id === mockBaseFontSizeVariable.id) return mockBaseFontSizeVariable;
455+
return null;
456+
});
457+
458+
// Call updateVariables for Theme A (Mode A)
459+
await updateVariables({
460+
collection,
461+
mode: 'mode-a',
462+
theme: themeA,
463+
tokens,
464+
settings,
465+
overallConfig: { core: TokenSetStatus.ENABLED },
466+
});
467+
468+
// Call updateVariables for Theme B (Mode B)
469+
await updateVariables({
470+
collection,
471+
mode: 'mode-b',
472+
theme: themeB,
473+
tokens,
474+
settings,
475+
overallConfig: {
476+
core: TokenSetStatus.ENABLED,
477+
dark: TokenSetStatus.ENABLED,
478+
},
479+
});
480+
481+
// Verify spacing/small got 16px for Mode A (1rem * 16)
482+
// Note: It might not be called if value is already 16, but we check consistency
483+
expect(mockSpacingSetValueForMode).toHaveBeenCalledWith('mode-a', 16);
484+
// Verify spacing/small got 20px for Mode B (1rem * 20)
485+
expect(mockSpacingSetValueForMode).toHaveBeenCalledWith('mode-b', 20);
486+
487+
// Verify base/font-size updates
488+
expect(mockBaseFontSizeSetValueForMode).toHaveBeenCalledWith('mode-a', 16);
489+
expect(mockBaseFontSizeSetValueForMode).toHaveBeenCalledWith('mode-b', 20);
490+
});
300491
});

packages/tokens-studio-for-figma/src/plugin/updateVariables.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,12 @@ export default async function updateVariables({
5353
let themeBaseFontSize = settings.baseFontSize;
5454
if (settings.aliasBaseFontSize) {
5555
const resolvedBaseFontSize = getAliasValue(settings.aliasBaseFontSize, resolvedTokens);
56-
if (resolvedBaseFontSize && typeof resolvedBaseFontSize === 'string') {
57-
themeBaseFontSize = resolvedBaseFontSize;
56+
if (
57+
resolvedBaseFontSize !== undefined
58+
&& resolvedBaseFontSize !== null
59+
&& (typeof resolvedBaseFontSize === 'string' || typeof resolvedBaseFontSize === 'number')
60+
) {
61+
themeBaseFontSize = String(resolvedBaseFontSize);
5862
}
5963
}
6064

0 commit comments

Comments
 (0)