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:
parent
ba9886beb4
commit
8a39949ed0
5 changed files with 157 additions and 66 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
103
packages/core/src/lib/outputters/json/index.ts
Normal file
103
packages/core/src/lib/outputters/json/index.ts
Normal 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
|
6
packages/core/src/lib/outputters/json/syntax.ts
Normal file
6
packages/core/src/lib/outputters/json/syntax.ts
Normal 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)
|
||||
}
|
45
packages/core/src/lib/outputters/json/types.ts
Normal file
45
packages/core/src/lib/outputters/json/types.ts
Normal 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']
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue