Open C3PablO opened 1 year ago
Feel free to contribute :) let me know if you need help!
Here's a formatter in a gist which adds a letter spacing token after a typography font css shorthand token.
It would be really cool to expand the typography token into all of its tokens (as per the expand
option), and have the short hand reference the expanded properties.
That would provide ultimate flexibility and themability. I'm not sure if we need that though, so I haven't implemented it.
--type-100-weight: 800;
--type-100-size: 10px;
--type-100-line-height: 16px;
--type-100-font-family: var(--font-nunito-sans);
--type-100: var(--type-100-weight) var(--type-100-size)/var(--type-100-line-height) var(--type-100-font-family);
Here's another gist. It moves the expansion logic into its own class, where it can be called from a custom formatter.
just my 2 cents as I ran into a similar issue with [Object object]
being printed for these typography fields.
This splits them into individual fields and removes the main token from the output (could be configured to keep if needed).
/**
* Expands typography whilst keeping references to the original typography object.
*
* This formatter creates an individual token for each of the typography token keys.
* For example:
* --token-key-font-family: var(--global-reference-font-family);
* --token-key-font-weight: var(--global-reference-font-weight);
* --token-key-line-height: var(--global-reference-line-height);
* --token-key-font-size: var(--global-reference-font-size);
* --token-key-letter-spacing: var(--global-reference-letter-spacing);
* --token-key-paragraph-spacing: var(--global-reference-paragraph-spacing);
* --token-key-text-case: var(--global-reference-text-case);
* --token-key-text-decoration: var(--global-reference-text-decoration);
*/
StyleDictionary.registerFormat({
name: 'css/customFormatter',
formatter: function ({ dictionary, platform, options, file }) {
const { selector, outputReferences } = options
const basePropertyFormatter = createPropertyFormatter({
outputReferences,
dictionary,
format: 'css',
})
function propertyFormatter(token) {
const cssVariables = []
if (token.type === 'typography') {
const values = {
fontFamily: {
name: 'font-family',
value: getByTypographyKey(token, 'fontFamily'),
},
fontWeight: {
name: 'font-weight',
value: getByTypographyKey(token, 'fontWeight'),
},
lineHeight: {
name: 'line-height',
value: getByTypographyKey(token, 'lineHeight'),
},
fontSize: {
name: 'font-size',
value: getByTypographyKey(token, 'fontSize'),
},
letterSpacing: {
name: 'letter-spacing',
value: getByTypographyKey(token, 'letterSpacing'),
},
paragraphSpacing: {
name: 'paragraph-spacing',
value: getByTypographyKey(token, 'paragraphSpacing'),
},
textCase: {
name: 'text-case',
value: getByTypographyKey(token, 'textCase'),
},
textDecoration: {
name: 'text-decoration',
value: getByTypographyKey(token, 'textDecoration'),
},
}
Object.keys(values).forEach((typographyKey) => {
const data = values[typographyKey]
if (data.value) {
cssVariables.push(` --${token.name}-${data.name}: ${data.value};`)
}
})
} else {
// run for 'typography' types too if you wish to keep the original token here
cssVariables.push(basePropertyFormatter(token))
}
return cssVariables.join('\n')
}
return `${fileHeader({ file })}${selector} {
${dictionary.allTokens.map(propertyFormatter).join('\n')}
}`
function getByTypographyKey(token, key) {
const original = token.original
let value = ''
/*
* A typography token can either be an object, which defines each part (font size, line height, etc).
* Or it might be an alias (a string) which references another token.
*/
// If it contains the data we need, use that data.
if (typeof original.value == 'object') {
value = getValue(token, key)
} else {
// If it's a reference, get the value that it's referring to.
// Note that if outputReferences is true, instead of returning the source tokens value, it'll return a
// var(--...) based on the source tokens name.
if (!dictionary.usesReference(original.value)) {
throw 'Typography string must be a reference?'
}
value = getValue(dictionary.getReferences(original.value)[0], key)
}
return value
}
function getValue(token, key) {
if (!(key in token.original.value)) {
return ''
}
const originalValueForKey = token.original.value[key]
if (dictionary.usesReference(originalValueForKey)) {
const source = dictionary.getReferences(originalValueForKey)[0]
return options.outputReferences ? `var(--${source.name})` : source.value
}
return originalValueForKey
}
},
})
I did some more investigation and the short answer to this issue is that this is not at all trivial to do because we're mixing up the concept of a token group
with a token
.
Imagine this typography token:
{
"foo": {
"value": {
"fontSize": "16px",
"fontFamily": "Arial",
"fontWeight": "Bold",
"lineHeight": "1"
},
"type": "typography"
}
}
After expanding this becomes:
{
"foo": {
"fontSize": {
"value": "16px",
"type": "fontSizes"
},
"fontFamily": {
"value": "Arial",
"type": "fontFamilies"
},
"fontWeight": {
"value": "Bold",
"type": "fontWeights"
},
"lineHeight": {
"value": "1",
"type": "lineHeights"
}
}
}
Now let's imagine we want to keep the original composite value as well, the structure would look something like:
{
"foo": {
"fontSize": {
"value": "16px",
"type": "fontSizes"
},
"fontFamily": {
"value": "Arial",
"type": "fontFamilies"
},
"fontWeight": {
"value": "Bold",
"type": "fontWeights"
},
"lineHeight": {
"value": "1",
"type": "lineHeights"
}
},
"value": {
"fontSize": "16px",
"fontFamily": "Arial",
"fontWeight": "Bold",
"lineHeight": "1"
},
"type": "typography"
}
Making foo
both a token group and a token (because it has a value and type property), that just doesn't work, it trips up Style-Dictionary and rightfully so.
A potential solution is to make the structure something like this:
{
"foo": {
"fontSize": {
"value": "16px",
"type": "fontSizes"
},
"fontFamily": {
"value": "Arial",
"type": "fontFamilies"
},
"fontWeight": {
"value": "Bold",
"type": "fontWeights"
},
"lineHeight": {
"value": "1",
"type": "lineHeights"
},
"__sd_keep_original__": {
"value": {
"fontSize": "16px",
"fontFamily": "Arial",
"fontWeight": "Bold",
"lineHeight": "1"
},
"type": "typography"
}
}
}
Which would result in
:root {
--foo-font-size: 16px;
--foo-font-family: Arial;
--foo-font-weight: 700;
--foo-line-height: 1;
--foo-sd-keep-original: 700 16px/1 Arial;
}
So the only thing that's left to do in sd-transforms is to register a name transform by default takes into account the casing
option that you can pass to registerTransforms
, which then filters out this sd_keep_original path item (because we don't want this in the output):
import { kebabCase, camelCase, capitalCase, snakeCase, constantCase } from 'change-case';
const casingMap = new Map([
['kebab', kebabCase],
['camel', camelCase],
['pascal', capitalCase],
['snake', snakeCase],
['constant', constantCase],
]);
StyleDictionary.registerTransform({
type: "name",
name: "ts/name",
transformer: (token) => {
return casingMap.get(casing)(token.path.filter(p => p !== '__sd_keep_original__').join('_'));
}
});
Which then results in:
:root {
--foo-font-size: 16px;
--foo-font-family: Arial;
--foo-font-weight: 700;
--foo-line-height: 1;
--foo: 700 16px/1 Arial;
}
Let me know if this makes sense, then I can go ahead and implement this when I get prio for it, or someone can speed it along by doing it themselves, hopefully it's clear enough from my explanation how to implement it, feel free to send me a message on slack if you need help
Marking as external since Style Dictionary has its own expand utils now for composite type tokens, so this feature should be added there.
What feature would you like?
The reason we want this is so we can still use the typography shorthand as we find it very handy but also use the css variable for letter spacing which value doesn't exist in the shorthand.
maybe an expand.keepOriginal option with the same signature (true|false|Filter )
Threat in Slack: https://tokens-studio.slack.com/archives/C0336AEQ06Q/p1684318079922049
Would you be available to contribute this feature?