Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature]: Allow having both the output of ts/typography/css/shorthand and expand.typography = true #128

Open
1 task done
C3PablO opened this issue May 17, 2023 · 6 comments
Labels
enhancement New feature or request external Related to another repository, e.g. Style-Dictionary

Comments

@C3PablO
Copy link

C3PablO commented May 17, 2023

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?

  • Yes, I would like to contribute this
@C3PablO C3PablO added the enhancement New feature or request label May 17, 2023
@jorenbroekema
Copy link
Member

Feel free to contribute :) let me know if you need help!

@yringler
Copy link
Contributor

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);

@yringler
Copy link
Contributor

Here's another gist. It moves the expansion logic into its own class, where it can be called from a custom formatter.

@lukethacoder
Copy link

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
    }
  },
})

@jorenbroekema
Copy link
Member

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

@jorenbroekema jorenbroekema added the external Related to another repository, e.g. Style-Dictionary label May 13, 2024
@jorenbroekema
Copy link
Member

Marking as external since Style Dictionary has its own expand utils now for composite type tokens, so this feature should be added there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request external Related to another repository, e.g. Style-Dictionary
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants