Skip to content

Commit da98eb3

Browse files
committed
Support variable in typography
1 parent d25e109 commit da98eb3

File tree

7 files changed

+202
-17
lines changed

7 files changed

+202
-17
lines changed

src/commands/devup/__tests__/index.test.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ describe('devup commands', () => {
147147
textStyleToTypographySpy = spyOn(
148148
textStyleToTypographyModule,
149149
'textStyleToTypography',
150-
).mockReturnValue({
150+
).mockResolvedValue({
151151
fontFamily: 'Inter',
152152
} as unknown as DevupTypography)
153153

@@ -201,7 +201,7 @@ describe('devup commands', () => {
201201
textStyleToTypographySpy = spyOn(
202202
textStyleToTypographyModule,
203203
'textStyleToTypography',
204-
).mockReturnValue(typoSeg as unknown as DevupTypography)
204+
).mockResolvedValue(typoSeg as unknown as DevupTypography)
205205
textSegmentToTypographySpy = spyOn(
206206
textSegmentToTypographyModule,
207207
'textSegmentToTypography',
@@ -263,7 +263,7 @@ describe('devup commands', () => {
263263
textStyleToTypographySpy = spyOn(
264264
textStyleToTypographyModule,
265265
'textStyleToTypography',
266-
).mockReturnValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
266+
).mockResolvedValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
267267

268268
const mixedSymbol = Symbol('mixed')
269269
const mixedTextNode = {
@@ -314,7 +314,7 @@ describe('devup commands', () => {
314314
textStyleToTypographySpy = spyOn(
315315
textStyleToTypographyModule,
316316
'textStyleToTypography',
317-
).mockReturnValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
317+
).mockResolvedValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
318318

319319
const currentTextNode = {
320320
type: 'TEXT',
@@ -387,7 +387,7 @@ describe('devup commands', () => {
387387
textStyleToTypographySpy = spyOn(
388388
textStyleToTypographyModule,
389389
'textStyleToTypography',
390-
).mockReturnValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
390+
).mockResolvedValue({ fontFamily: 'Inter' } as unknown as DevupTypography)
391391

392392
const currentSectionFindAllWithCriteria = mock(() => [])
393393
const otherTextNode = {
@@ -463,7 +463,8 @@ describe('devup commands', () => {
463463
textStyleToTypographyModule,
464464
'textStyleToTypography',
465465
).mockImplementation(
466-
(style: TextStyle) => ({ id: style.id }) as unknown as DevupTypography,
466+
async (style: TextStyle) =>
467+
({ id: style.id }) as unknown as DevupTypography,
467468
)
468469

469470
const directTextNode = {
@@ -535,7 +536,7 @@ describe('devup commands', () => {
535536
textStyleToTypographySpy = spyOn(
536537
textStyleToTypographyModule,
537538
'textStyleToTypography',
538-
).mockReturnValue(typoSeg as unknown as DevupTypography)
539+
).mockResolvedValue(typoSeg as unknown as DevupTypography)
539540

540541
const textNode = {
541542
type: 'TEXT',
@@ -591,7 +592,8 @@ describe('devup commands', () => {
591592
textStyleToTypographyModule,
592593
'textStyleToTypography',
593594
).mockImplementation(
594-
(style: TextStyle) => ({ id: style.id }) as unknown as DevupTypography,
595+
async (style: TextStyle) =>
596+
({ id: style.id }) as unknown as DevupTypography,
595597
)
596598

597599
;(globalThis as { figma?: unknown }).figma = {
@@ -637,7 +639,8 @@ describe('devup commands', () => {
637639
textStyleToTypographyModule,
638640
'textStyleToTypography',
639641
).mockImplementation(
640-
(style: TextStyle) => ({ id: style.id }) as unknown as DevupTypography,
642+
async (style: TextStyle) =>
643+
({ id: style.id }) as unknown as DevupTypography,
641644
)
642645

643646
;(globalThis as { figma?: unknown }).figma = {

src/commands/devup/export-devup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export async function buildDevupConfig(
215215
allTypographyKeyCount += 1
216216
}
217217
if (!typographyValues[meta.level]) {
218-
typographyValues[meta.level] = textStyleToTypography(style)
218+
typographyValues[meta.level] = await textStyleToTypography(style)
219219
}
220220
styleMetaById[style.id] = meta
221221
}

src/commands/devup/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export interface DevupTypography {
22
fontFamily?: string
33
fontStyle?: string
44
fontSize?: string
5-
fontWeight?: number
5+
fontWeight?: number | string
66
lineHeight?: number | string
77
letterSpacing?: string
88
textDecoration?: string
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { afterEach, describe, expect, test } from 'bun:test'
2+
import { resolveTextStyleBoundVariable } from '../resolve-text-style-bound-variable'
3+
4+
describe('resolveTextStyleBoundVariable', () => {
5+
afterEach(() => {
6+
;(globalThis as any).figma = undefined
7+
})
8+
9+
test('returns null when boundVariables is undefined', async () => {
10+
expect(
11+
await resolveTextStyleBoundVariable(undefined, 'fontSize'),
12+
).toBeNull()
13+
})
14+
15+
test('returns null when field is not bound', async () => {
16+
expect(
17+
await resolveTextStyleBoundVariable({} as any, 'fontSize'),
18+
).toBeNull()
19+
})
20+
21+
test('returns null when variable is not found', async () => {
22+
;(globalThis as any).figma = {
23+
variables: {
24+
getVariableByIdAsync: async () => null,
25+
},
26+
}
27+
expect(
28+
await resolveTextStyleBoundVariable(
29+
{ fontSize: { type: 'VARIABLE_ALIAS', id: 'var1' } } as any,
30+
'fontSize',
31+
),
32+
).toBeNull()
33+
})
34+
35+
test('returns $camelName when variable is found', async () => {
36+
;(globalThis as any).figma = {
37+
variables: {
38+
getVariableByIdAsync: async () => ({ name: 'heading/font-size' }),
39+
},
40+
}
41+
expect(
42+
await resolveTextStyleBoundVariable(
43+
{ fontSize: { type: 'VARIABLE_ALIAS', id: 'var1' } } as any,
44+
'fontSize',
45+
),
46+
).toBe('$headingFontSize')
47+
})
48+
49+
test('returns null when variable has no name', async () => {
50+
;(globalThis as any).figma = {
51+
variables: {
52+
getVariableByIdAsync: async () => ({ name: '' }),
53+
},
54+
}
55+
expect(
56+
await resolveTextStyleBoundVariable(
57+
{ fontSize: { type: 'VARIABLE_ALIAS', id: 'var1' } } as any,
58+
'fontSize',
59+
),
60+
).toBeNull()
61+
})
62+
})

src/utils/__tests__/text-style-to-typography.test.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { describe, expect, test } from 'bun:test'
1+
import { afterEach, describe, expect, test } from 'bun:test'
22
import { textStyleToTypography } from '../text-style-to-typography'
33

4-
function makeStyle(styleName: string): TextStyle {
4+
function makeStyle(
5+
styleName: string,
6+
boundVariables?: Record<string, any>,
7+
): TextStyle {
58
return {
69
id: 'style',
710
name: 'style',
@@ -19,10 +22,15 @@ function makeStyle(styleName: string): TextStyle {
1922
textAlignVertical: 'TOP',
2023
lineHeight: { unit: 'AUTO' },
2124
letterSpacing: { unit: 'PIXELS', value: 0 },
25+
boundVariables,
2226
} as unknown as TextStyle
2327
}
2428

2529
describe('textStyleToTypography', () => {
30+
afterEach(() => {
31+
;(globalThis as any).figma = undefined
32+
})
33+
2634
test.each([
2735
['Thin', 100],
2836
['Extra Light', 200],
@@ -40,8 +48,63 @@ describe('textStyleToTypography', () => {
4048
['Heavy', 900],
4149
['750', 750],
4250
['UnknownWeight', 400],
43-
])('maps %s to fontWeight %d', (styleName, expected) => {
44-
const result = textStyleToTypography(makeStyle(styleName))
51+
])('maps %s to fontWeight %d', async (styleName, expected) => {
52+
const result = await textStyleToTypography(makeStyle(styleName))
4553
expect(result.fontWeight).toBe(expected)
4654
})
55+
56+
test('returns base typography when no boundVariables', async () => {
57+
const result = await textStyleToTypography(makeStyle('Regular'))
58+
expect(result.fontFamily).toBe('Pretendard')
59+
expect(result.fontSize).toBe('16px')
60+
})
61+
62+
test('overrides fields with bound variable references', async () => {
63+
;(globalThis as any).figma = {
64+
variables: {
65+
getVariableByIdAsync: async (id: string) => {
66+
const vars: Record<string, any> = {
67+
v1: { name: 'heading/size' },
68+
v2: { name: 'heading/line-height' },
69+
v3: { name: 'heading/spacing' },
70+
v4: { name: 'heading/weight' },
71+
v5: { name: 'heading/family' },
72+
v6: { name: 'heading/style' },
73+
}
74+
return vars[id] ?? null
75+
},
76+
},
77+
}
78+
const result = await textStyleToTypography(
79+
makeStyle('Regular', {
80+
fontSize: { type: 'VARIABLE_ALIAS', id: 'v1' },
81+
lineHeight: { type: 'VARIABLE_ALIAS', id: 'v2' },
82+
letterSpacing: { type: 'VARIABLE_ALIAS', id: 'v3' },
83+
fontWeight: { type: 'VARIABLE_ALIAS', id: 'v4' },
84+
fontFamily: { type: 'VARIABLE_ALIAS', id: 'v5' },
85+
fontStyle: { type: 'VARIABLE_ALIAS', id: 'v6' },
86+
}),
87+
)
88+
expect(result.fontSize).toBe('$headingSize')
89+
expect(result.lineHeight).toBe('$headingLineHeight')
90+
expect(result.letterSpacing).toBe('$headingSpacing')
91+
expect(result.fontWeight).toBe('$headingWeight')
92+
expect(result.fontFamily).toBe('$headingFamily')
93+
expect(result.fontStyle).toBe('$headingStyle')
94+
})
95+
96+
test('keeps base value when variable not found', async () => {
97+
;(globalThis as any).figma = {
98+
variables: {
99+
getVariableByIdAsync: async () => null,
100+
},
101+
}
102+
const result = await textStyleToTypography(
103+
makeStyle('Bold', {
104+
fontSize: { type: 'VARIABLE_ALIAS', id: 'missing' },
105+
}),
106+
)
107+
expect(result.fontSize).toBe('16px')
108+
expect(result.fontWeight).toBe(700)
109+
})
47110
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { toCamel } from './to-camel'
2+
3+
type VariableBindableTextField =
4+
| 'fontFamily'
5+
| 'fontSize'
6+
| 'fontStyle'
7+
| 'fontWeight'
8+
| 'letterSpacing'
9+
| 'lineHeight'
10+
| 'paragraphSpacing'
11+
| 'paragraphIndent'
12+
13+
type TextStyleBoundVariables = {
14+
[field in VariableBindableTextField]?: VariableAlias
15+
}
16+
17+
export async function resolveTextStyleBoundVariable(
18+
boundVariables: TextStyleBoundVariables | undefined,
19+
field: VariableBindableTextField,
20+
): Promise<string | null> {
21+
const binding = boundVariables?.[field]
22+
if (!binding) return null
23+
const variable = await figma.variables.getVariableByIdAsync(binding.id)
24+
if (variable?.name) return `$${toCamel(variable.name)}`
25+
return null
26+
}

src/utils/text-style-to-typography.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { DevupTypography } from '../commands/devup/types'
2+
import { resolveTextStyleBoundVariable } from './resolve-text-style-bound-variable'
23
import { textSegmentToTypography } from './text-segment-to-typography'
34
import { toCamel } from './to-camel'
45

5-
export function textStyleToTypography(style: TextStyle): DevupTypography {
6-
return textSegmentToTypography({
6+
export async function textStyleToTypography(
7+
style: TextStyle,
8+
): Promise<DevupTypography> {
9+
const base = textSegmentToTypography({
710
fontName: style.fontName,
811
fontWeight: getFontWeight(style.fontName.style),
912
fontSize: style.fontSize,
@@ -12,6 +15,34 @@ export function textStyleToTypography(style: TextStyle): DevupTypography {
1215
lineHeight: style.lineHeight,
1316
letterSpacing: style.letterSpacing,
1417
})
18+
19+
const boundVars = style.boundVariables
20+
if (!boundVars) return base
21+
22+
const [
23+
fontFamily,
24+
fontSize,
25+
fontStyle,
26+
fontWeight,
27+
letterSpacing,
28+
lineHeight,
29+
] = await Promise.all([
30+
resolveTextStyleBoundVariable(boundVars, 'fontFamily'),
31+
resolveTextStyleBoundVariable(boundVars, 'fontSize'),
32+
resolveTextStyleBoundVariable(boundVars, 'fontStyle'),
33+
resolveTextStyleBoundVariable(boundVars, 'fontWeight'),
34+
resolveTextStyleBoundVariable(boundVars, 'letterSpacing'),
35+
resolveTextStyleBoundVariable(boundVars, 'lineHeight'),
36+
])
37+
38+
if (fontFamily) base.fontFamily = fontFamily
39+
if (fontSize) base.fontSize = fontSize
40+
if (fontStyle) base.fontStyle = fontStyle
41+
if (fontWeight) base.fontWeight = fontWeight
42+
if (letterSpacing) base.letterSpacing = letterSpacing
43+
if (lineHeight) base.lineHeight = lineHeight
44+
45+
return base
1546
}
1647

1748
function getFontWeight(weight: string): number {

0 commit comments

Comments
 (0)