0
Fork 0
mirror of https://github.com/penpot/penpot-export.git synced 2025-03-11 23:11:17 -05:00

feat!(core): follow W3C draft design tokens format spec for colors and typographies in JSON output

Draft spec (24 July 2023): https://tr.designtokens.org/format/

Deprecate JSON output for page components, since the spec is not flexible enough for arbitrary properties.
This commit is contained in:
Roberto Redradix 2023-09-12 13:33:16 +02:00
parent ba9886beb4
commit 8a39949ed0
5 changed files with 157 additions and 66 deletions

View file

@ -48,6 +48,9 @@ const colorsConfigSchema = assetConfigSchema
const typographiesConfigSchema = assetConfigSchema
const pagesConfigSchema = assetConfigSchema.extend({
pageId: pageIdSchema,
format: z
.optional(z.union([z.literal('css'), z.literal('scss')]))
.default('css'),
})
const userFileConfigSchema = z

View file

@ -1,66 +0,0 @@
import {
CSSClassDefinition,
CSSCustomPropertyDefinition,
ColorAssets,
PageComponentAssets,
TypographyAssets,
} from '../types'
import { PenpotExportInvalidAssetsError } from './errors'
import { OutputterFunction } from './types'
interface JSONCustomPropertyDefinition extends CSSCustomPropertyDefinition {
scope: string
}
interface JSONClassDefinition extends CSSClassDefinition {
scope: string
}
const appendScopeToCustomProperties = (
cssCustomPropertiesDefinitions: CSSCustomPropertyDefinition[],
scope: string,
): JSONCustomPropertyDefinition[] => {
return cssCustomPropertiesDefinitions.map<JSONCustomPropertyDefinition>(
(customProperty) => {
return {
scope,
...customProperty,
}
},
)
}
const appendScopeToClasses = (
cssClassDefinition: CSSClassDefinition[],
scope: string,
): JSONClassDefinition[] => {
return cssClassDefinition.map<JSONClassDefinition>((classDefinition) => {
return {
scope,
...classDefinition,
}
})
}
const serializeJson: OutputterFunction = ({
scope,
colors,
typographies,
pageComponents,
}: ColorAssets | TypographyAssets | PageComponentAssets): string => {
if (colors) {
const scopedColors = appendScopeToCustomProperties(colors, scope)
return JSON.stringify(scopedColors, null, 2)
} else if (typographies || pageComponents) {
const scopedAssets = appendScopeToClasses(
typographies ?? pageComponents,
scope,
)
return JSON.stringify(scopedAssets, null, 2)
}
throw new PenpotExportInvalidAssetsError()
}
export default serializeJson

View file

@ -0,0 +1,103 @@
import {
CSSClassDefinition,
CSSCustomPropertyDefinition,
PenpotExportAssets,
} from '../../types'
import { PenpotExportInvalidAssetsError } from '../errors'
import { OutputterFunction } from '../types'
import { textToTokenName } from './syntax'
const transformCssCustomPropertyToColorTokenEntry = (
color: CSSCustomPropertyDefinition,
): [string, ColorToken] => {
const key = textToTokenName(color.name)
const value: ColorToken = {
$description: color.name,
$type: 'color',
$value: color.value as ColorToken['$value'],
}
return [key, value]
}
const transformCssClassDefinitionToTypographyCompositeTokenEntry = (
typography: CSSClassDefinition,
): [string, TypographyCompositeToken] => {
const key = textToTokenName(typography.name)
const unsupportedPropNames: string[] = []
const value = Object.entries(typography.cssProps).reduce<
TypographyCompositeToken['$value']
>((tokens, [propName, propValue]) => {
switch (propName) {
case 'fontFamily': {
tokens.fontFamily = JSON.parse(propValue)
break
}
case 'fontSize': {
tokens.fontSize = propValue as DimensionToken['$value']
break
}
case 'fontWeight': {
tokens.fontWeight = parseInt(propValue, 10)
break
}
case 'letterSpacing': {
tokens.letterSpacing = propValue as DimensionToken['$value']
break
}
case 'lineHeight': {
tokens.lineHeight = parseFloat(propValue)
break
}
default: {
unsupportedPropNames.push(propName)
}
}
return tokens
}, {} as any)
console.error(
`Unsupported CSS properties: ${unsupportedPropNames.join(', ')}`,
)
return [
key,
{
$description: typography.name,
$type: 'typography',
$value: value,
},
]
}
const composeTokensEntries = ({
colors,
typographies,
}: PenpotExportAssets):
| [string, ColorToken][]
| [string, TypographyCompositeToken][] => {
if (colors) {
return colors.map(transformCssCustomPropertyToColorTokenEntry)
} else if (typographies) {
return typographies.map(
transformCssClassDefinitionToTypographyCompositeTokenEntry,
)
}
throw new PenpotExportInvalidAssetsError()
}
const serializeJson: OutputterFunction = (
assets: PenpotExportAssets,
): string => {
const tokensEntries = composeTokensEntries(assets)
const content: object = {
$description: assets.scope,
...Object.fromEntries(tokensEntries),
}
return JSON.stringify(content, null, 2)
}
export default serializeJson

View file

@ -0,0 +1,6 @@
import { textToCssIdentToken } from '../css/syntax'
// REVIEW https://tr.designtokens.org/format/#character-restrictions
export const textToTokenName = (str: string): string => {
return textToCssIdentToken(str)
}

View file

@ -0,0 +1,45 @@
interface BaseToken {
$description?: string
}
interface ColorToken extends BaseToken {
$type: 'color'
$value: `#${string}`
}
interface DimensionToken extends BaseToken {
$type: 'dimension'
$value: `${number}px` | `${number}rem`
}
interface FontFamilyToken extends BaseToken {
$type: 'fontFamily'
$value: string
}
interface FontWeightToken extends BaseToken {
$type: 'fontWeight'
$value:
| number
| ('thin' | 'hairline')
| ('extra-light' | 'ultra-light')
| 'light'
| ('normal' | 'regular' | 'book')
| 'medium'
| ('semi-bold' | 'demi-bold')
| 'bold'
| ('extra-bold' | 'ultra-bold')
| ('black' | 'heavy')
| ('extra-black' | 'ultra-black')
}
interface NumberToken extends BaseToken {
$type: 'number'
$value: number
}
interface TypographyCompositeToken extends BaseToken {
$type: 'typography'
$value: {
fontFamily: FontFamilyToken['$value']
fontSize: DimensionToken['$value']
fontWeight: FontWeightToken['$value']
letterSpacing: DimensionToken['$value']
lineHeight: NumberToken['$value']
}
}