Skip to content

Commit e10f9b5

Browse files
committed
Refactor
1 parent 123b6b8 commit e10f9b5

File tree

3 files changed

+301
-286
lines changed

3 files changed

+301
-286
lines changed

src/commands/devup/export-devup.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { downloadFile } from '../../utils/download-file'
2+
import { isVariableAlias } from '../../utils/is-variable-alias'
3+
import { optimizeHex } from '../../utils/optimize-hex'
4+
import { rgbaToHex } from '../../utils/rgba-to-hex'
5+
import { styleNameToTypography } from '../../utils/style-name-to-typography'
6+
import { textSegmentToTypography } from '../../utils/text-segment-to-typography'
7+
import { textStyleToTypography } from '../../utils/text-style-to-typography'
8+
import { toCamel } from '../../utils/to-camel'
9+
import { variableAliasToValue } from '../../utils/variable-alias-to-value'
10+
import type { Devup, DevupTypography } from './types'
11+
import { downloadDevupXlsx } from './utils/download-devup-xlsx'
12+
import { getDevupColorCollection } from './utils/get-devup-color-collection'
13+
14+
export async function exportDevup(
15+
output: 'json' | 'excel',
16+
treeshaking: boolean = true,
17+
) {
18+
const devup: Devup = {}
19+
20+
const collection = await getDevupColorCollection()
21+
if (collection) {
22+
for (const mode of collection.modes) {
23+
devup.theme ??= {}
24+
devup.theme.colors ??= {}
25+
const colors: Record<string, string> = {}
26+
devup.theme.colors[mode.name.toLowerCase()] = colors
27+
await Promise.all(
28+
collection.variableIds.map(async (varId) => {
29+
const variable = await figma.variables.getVariableByIdAsync(varId)
30+
if (variable === null) return
31+
const value = variable.valuesByMode[mode.modeId]
32+
if (typeof value === 'boolean' || typeof value === 'number') return
33+
if (isVariableAlias(value)) {
34+
const nextValue = await variableAliasToValue(value, mode.modeId)
35+
if (nextValue === null) return
36+
if (typeof nextValue === 'boolean' || typeof nextValue === 'number')
37+
return
38+
colors[toCamel(variable.name)] = optimizeHex(
39+
rgbaToHex(figma.util.rgba(nextValue)),
40+
)
41+
} else {
42+
colors[toCamel(variable.name)] = optimizeHex(
43+
rgbaToHex(figma.util.rgba(value)),
44+
)
45+
}
46+
}),
47+
)
48+
}
49+
}
50+
51+
await figma.loadAllPagesAsync()
52+
53+
const textStyles = await figma.getLocalTextStylesAsync()
54+
const ids = new Set()
55+
const styles: Record<string, TextStyle> = {}
56+
for (const style of textStyles) {
57+
ids.add(style.id)
58+
styles[style.name] = style
59+
}
60+
61+
const typography: Record<string, (null | DevupTypography)[]> = {}
62+
if (treeshaking) {
63+
const texts = figma.root.findAllWithCriteria({ types: ['TEXT'] })
64+
await Promise.all(
65+
texts
66+
.filter(
67+
(text) =>
68+
(typeof text.textStyleId === 'string' && text.textStyleId) ||
69+
text.textStyleId === figma.mixed,
70+
)
71+
.map(async (text) => {
72+
for (const seg of text.getStyledTextSegments([
73+
'fontName',
74+
'fontWeight',
75+
'fontSize',
76+
'textDecoration',
77+
'textCase',
78+
'lineHeight',
79+
'letterSpacing',
80+
'fills',
81+
'textStyleId',
82+
'fillStyleId',
83+
'listOptions',
84+
'indentation',
85+
'hyperlink',
86+
])) {
87+
if (seg?.textStyleId) {
88+
const style = await figma.getStyleByIdAsync(seg.textStyleId)
89+
90+
if (!(style && ids.has(style.id))) continue
91+
const { level, name } = styleNameToTypography(style.name)
92+
const typo = textSegmentToTypography(seg)
93+
if (typography[name]?.[level]) continue
94+
typography[name] ??= [null, null, null, null, null, null]
95+
typography[name][level] = typo
96+
}
97+
}
98+
}),
99+
)
100+
} else {
101+
for (const [styleName, style] of Object.entries(styles)) {
102+
const { level, name } = styleNameToTypography(styleName)
103+
const typo = textStyleToTypography(style)
104+
if (typography[name]?.[level]) continue
105+
typography[name] ??= [null, null, null, null, null, null]
106+
typography[name][level] = typo
107+
}
108+
}
109+
110+
for (const [name, style] of Object.entries(styles)) {
111+
const { level, name: styleName } = styleNameToTypography(name)
112+
if (typography[styleName] && !typography[styleName][level]) {
113+
typography[styleName][level] = textStyleToTypography(style)
114+
}
115+
}
116+
117+
if (Object.keys(typography).length > 0) {
118+
devup.theme ??= {}
119+
devup.theme.typography = Object.entries(typography).reduce(
120+
(acc, [key, value]) => {
121+
const filtered = value.filter((v) => v !== null)
122+
if (filtered.length === 0) {
123+
return acc
124+
}
125+
if (filtered.length === 1) {
126+
acc[key] = filtered[0]
127+
return acc
128+
}
129+
if (value[0] === null) {
130+
acc[key] = [filtered[0]]
131+
for (let i = 1; i < value.length; i += 1) {
132+
acc[key].push(value[i])
133+
}
134+
while (acc[key][acc[key].length - 1] === null) {
135+
acc[key].pop()
136+
}
137+
return acc
138+
}
139+
acc[key] = value
140+
return acc
141+
},
142+
{} as Record<string, DevupTypography | (null | DevupTypography)[]>,
143+
)
144+
}
145+
146+
switch (output) {
147+
case 'json':
148+
return downloadFile('devup.json', JSON.stringify(devup))
149+
case 'excel':
150+
return downloadDevupXlsx('devup.xlsx', JSON.stringify(devup))
151+
}
152+
}

src/commands/devup/import-devup.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { uploadFile } from '../../utils/upload-file'
2+
import type { Devup, DevupTypography } from './types'
3+
import { getDevupColorCollection } from './utils/get-devup-color-collection'
4+
import { uploadDevupXlsx } from './utils/upload-devup-xlsx'
5+
6+
type TargetTypography = [target: string, typography: DevupTypography]
7+
const TYPO_PREFIX = ['mobile', '1', 'tablet', '3', 'desktop', '5'] as const
8+
9+
export async function importDevup(input: 'json' | 'excel') {
10+
const devup = await loadDevup(input)
11+
await importColors(devup)
12+
await importTypography(devup)
13+
}
14+
15+
async function loadDevup(input: 'json' | 'excel'): Promise<Devup> {
16+
return input === 'json'
17+
? JSON.parse(await uploadFile('.json'))
18+
: await uploadDevupXlsx()
19+
}
20+
21+
async function importColors(devup: Devup) {
22+
const colors = devup.theme?.colors
23+
if (!colors) return
24+
25+
const collection =
26+
(await getDevupColorCollection()) ??
27+
(await figma.variables.createVariableCollection('Devup Colors'))
28+
29+
const themes = new Set<string>()
30+
const colorNames = new Set<string>()
31+
32+
for (const [theme, value] of Object.entries(colors)) {
33+
const modeId =
34+
collection.modes.find((mode) => mode.name === theme)?.modeId ??
35+
collection.addMode(theme)
36+
37+
const variables = await figma.variables.getLocalVariablesAsync()
38+
for (const [colorKey, colorValue] of Object.entries(value)) {
39+
const variable =
40+
variables.find((variable) => variable.name === colorKey) ??
41+
figma.variables.createVariable(colorKey, collection, 'COLOR')
42+
43+
variable.setValueForMode(modeId, figma.util.rgba(colorValue))
44+
colorNames.add(colorKey)
45+
}
46+
themes.add(theme)
47+
}
48+
49+
for (const theme of collection.modes.filter(
50+
(mode) => !themes.has(mode.name),
51+
)) {
52+
collection.removeMode(theme.modeId)
53+
}
54+
55+
const variables = await figma.variables.getLocalVariablesAsync()
56+
for (const variable of variables.filter((v) => !colorNames.has(v.name))) {
57+
variable.remove()
58+
}
59+
}
60+
61+
async function importTypography(devup: Devup) {
62+
const typography = devup.theme?.typography
63+
if (!typography) return
64+
65+
const styles = await figma.getLocalTextStylesAsync()
66+
67+
for (const [style, value] of Object.entries(typography)) {
68+
const targetStyleNames = buildTargetStyleNames(style, value)
69+
for (const [target, typo] of targetStyleNames) {
70+
await applyTypography(target, typo, styles)
71+
}
72+
}
73+
}
74+
75+
function buildTargetStyleNames(
76+
style: string,
77+
value: DevupTypography | (DevupTypography | null)[],
78+
): TargetTypography[] {
79+
const targets: TargetTypography[] = []
80+
if (Array.isArray(value)) {
81+
value.forEach((typo, idx) => {
82+
if (!typo) return
83+
const prefix = TYPO_PREFIX[idx] ?? `${idx}`
84+
targets.push([`${prefix}/${style}`, typo])
85+
})
86+
return targets
87+
}
88+
targets.push([`mobile/${style}`, value])
89+
return targets
90+
}
91+
92+
async function applyTypography(
93+
target: string,
94+
typography: DevupTypography,
95+
styles: TextStyle[],
96+
) {
97+
const st = styles.find((s) => s.name === target) ?? figma.createTextStyle()
98+
st.name = target
99+
const fontFamily = {
100+
family: typography.fontFamily ?? 'Inter',
101+
style: typography.fontStyle === 'italic' ? 'Italic' : 'Regular',
102+
}
103+
104+
try {
105+
await figma.loadFontAsync(fontFamily)
106+
st.fontName = fontFamily
107+
if (typography.fontSize) st.fontSize = parseInt(typography.fontSize, 10)
108+
if (typography.letterSpacing) {
109+
st.letterSpacing = typography.letterSpacing.endsWith('em')
110+
? {
111+
unit: 'PERCENT',
112+
value: parseFloat(typography.letterSpacing),
113+
}
114+
: {
115+
unit: 'PIXELS',
116+
value: parseFloat(typography.letterSpacing) * 100,
117+
}
118+
}
119+
if (typography.lineHeight) {
120+
st.lineHeight =
121+
typography.lineHeight === 'normal'
122+
? { unit: 'AUTO' }
123+
: typeof typography.lineHeight === 'string'
124+
? {
125+
unit: 'PIXELS',
126+
value: parseInt(typography.lineHeight, 10),
127+
}
128+
: {
129+
unit: 'PERCENT',
130+
value: Math.round(typography.lineHeight / 10) / 10,
131+
}
132+
}
133+
if (typography.textTransform) {
134+
st.textCase = typography.textTransform.toUpperCase() as TextCase
135+
}
136+
if (typography.textDecoration) {
137+
st.textDecoration =
138+
typography.textDecoration.toUpperCase() as TextDecoration
139+
}
140+
} catch (error) {
141+
console.error('Failed to create text style', error)
142+
figma.notify(
143+
`Failed to create text style (${target}, ${fontFamily.family} - ${fontFamily.style})`,
144+
{ error: true },
145+
)
146+
}
147+
}

0 commit comments

Comments
 (0)