amzn / style-dictionary

A build system for creating cross-platform styles.
https://styledictionary.com
Apache License 2.0
3.87k stars 543 forks source link

Transitive transforms appear to be happening out of order #889

Closed KenjiCrosland closed 1 year ago

KenjiCrosland commented 1 year ago

I'm working on a refactor of our style-dictionary implementation. I'm working on applying alpha values through a transform rather than predefining values with alpha values.

It looks something like this:

In color.json:

{
  color: {
    text: {
      primary: {
        value: '{options.color.warm-grey-1150.value}',
        alpha: .75,
        category: 'color',
        docs: {
          category: 'colors',
          type: 'text',
          example: 'color',
          description: 'The default, primary text color',
        },
      }
   }
}

The value for warm-grey-1150 is #0C0B08 and is in another file.

I have already successfully created a simple alpha transform for scss, less, and js and it works just fine:

const tinycolor = require('tinycolor2');

module.exports = (StyleDictionary) => {
    StyleDictionary.registerTransform({
        name: 'color/alpha',
        type: 'value',
        transitive: true,
        matcher(prop) {
            return (prop.attributes.category === 'color') && prop.alpha;
        },
        transformer(prop) {
            const { value, alpha } = prop;

            let color = tinycolor(value);
            color.setAlpha(alpha)
            return color.toRgbString();
        },
    });
};

However, I'm stuck on the IOS UIColor transform. My initial approach was to convert the colors to a hex8 value, as those were the original values that we were converting. (We had a value already created which mapped to #0C0B08BF and just plugged that into UIColor).

So I created a separate transform for IOS to set the alpha value and then extended the UI-color transform to make it transitive.

const tinycolor = require('tinycolor2');

module.exports = (StyleDictionary) => {
    StyleDictionary.registerTransform({
        name: 'color/alpha-hex',
        type: 'value',
        transitive: true,
        matcher(prop) {
            return (prop.attributes.category === 'color') && prop.alpha;
        },
        transformer(prop) {
            let { value, alpha } = prop;
            let color = tinycolor(value);
            color.setAlpha(alpha);
            return color.toHex8String();
        },

    });
};

I'm working on a refactor of our style-dictionary implementation. I'm working on applying alpha values through a transform rather than predefining values with alpha values.

It looks something like this:

In color.json

{
  color: {
    text: {
      primary: {
        value: '{options.color.warm-grey-1150.value}',
        alpha: .75,
        category: 'color',
        docs: {
          category: 'colors',
          type: 'text',
          example: 'color',
          description: 'The default, primary text color',
        },
      }
   }
}

The value for warm-grey-1150 is #0C0B08 and is in another file.

I have already successfully created a simple alpha transform for scss, less, and js and it works just fine:

const tinycolor = require('tinycolor2');

module.exports = (StyleDictionary) => {
    StyleDictionary.registerTransform({
        name: 'color/alpha',
        type: 'value',
        transitive: true,
        matcher(prop) {
            return (prop.attributes.category === 'color') && prop.alpha;
        },
        transformer(prop) {
            const { value, alpha } = prop;

            let color = tinycolor(value);
            color.setAlpha(alpha)
            return color.toRgbString();
        },
    });
};

However, I'm stuck on the IOS UIColor transform. My initial approach was to convert the colors to a hex8 value, as those were the original values that we were converting. (We had a value already created which mapped to #0C0B08BF and just plugged that into UIColor).

So I created a separate transform for IOS to set the alpha value and then extended the UI-color transform to make it transitive.


const tinycolor = require('tinycolor2');

module.exports = (StyleDictionary) => {
    StyleDictionary.registerTransform({
        name: 'color/alpha-hex',
        type: 'value',
        transitive: true,
        matcher(prop) {
            return (prop.attributes.category === 'color') && prop.alpha;
        },
        transformer(prop) {
            let { value, alpha } = prop;
            let color = tinycolor(value);
            color.setAlpha(alpha);
            return color.toHex8String();
        },

    });
};

In the transform group I made sure that the alpha-hex transform happened before UIColor:

module.exports = (StyleDictionary) => {
  StyleDictionary.registerTransformGroup({
    name: 'custom/ios',
    transforms: [
      //Other non-color related transforms
      'color/alpha-hex',
      'color/UIColor-transitive',
      //Other non-color related transforms
    ],
  });
};

The results were strange, as all the UIColor values that happened to undergo the alpha transform had a red, green and blue value of zero, but the alpha value was set:

[UIColor colorWithRed:0.000f green:0.000f blue:0.000f alpha:0.749f]

I decided to experiment and tried using chroma-js instead of tinycolor2 and chroma threw up an error:

Error: unknown format: [UIColor colorWithRed:0.047f green:0.043f blue:0.031f alpha:1.000f]

(Apparently, tinycolor doesn't throw up an error when passed an invalid format and instead creates an instance of tinycolor with #000000 as its value.)

For some reason, the UIColor formatted values are already being piped to the alpha-hex transform, even though I specified that I wanted the alpha-hex transform to run before. I've tried several things like not running the transform if value.indexOf('UIColor') !== -1) and that didn't seem to work. I also copied/pasted the UIColor transform and tried to run my hex transform in the same transform function but that didn't seem to work either.

Any ideas on what I'm missing here?

6220119 commented 1 year ago

+1, this also happened to me, it seems the transitive transforms only work with web (CSS, Less) color format (because they are in the same format as the original definition in the JSON files). Any other platforms format, i.e., ios-swift, flutter, etc. are broken due to the order of transformation.

For example, the order of transformation would look like this, for ios-swift:

  1. resolve color.warm-grey-1150 -> #0C0B08 (keep as-is, went through transitive-transform to change the alpha, but no changes)
  2. resolve color.warm-grey-1150 -> UIColor(0.047, 0.043, 0.031, 1) (went through color/UIColorSwift transitive transform)
  3. resolve color.text.primary -> reference to color.warm-grey-1150 -> found UIColor(0.047, 0.043, 0.031, 1) instead of #0C0B08, -> run into color/UIColorSwift transitive transform -> tinycolor2 don't understand the format -> UIColor(0.000, 0.000, 0.000, 1)

Currently, I have to make a custom format and updating the alpha + output to platform specific (not using value transforms)

KenjiCrosland commented 1 year ago

I figured out the issue for me. We had a whole bunch of options structure like this:

These are one of our color options tokens that get filtered out of the final result. These options get used for tokens like color-text-primary

      'warm-grey-200': {
        value: '#f4f2ed',
        category: 'color',
      },

When we use a transitive transform we are going through every single transform in a transform group for each of the options. With ios UIColor the problem is the final result of the transform can't get transformed again because it's a different format.

So this is what solved it for us. We changed the options to bare values like so:

     'warm-grey-200': '#f4f2ed',
      'warm-grey-300': '#edeae3',

we removed the "value" keyword as well as the category. However, this meant that now we had to make all of our transforms transitive (at least those dealing with color) and this meant extending some of the existing predefined transforms so they were transitive like so:

module.exports = (StyleDictionary) => {
    StyleDictionary.registerTransform(
        Object.assign({}, StyleDictionary.transform[`predefined/transform`], {
            name: 'predefined/transform-transitive',
            transitive: true
        }),
    )
}

This worked great for us. To sum up the two steps we needed to make were:

  1. Restructure our color options to bare values so that transforms don't happen to them until AFTER they are referenced
  2. Make sure that all transforms that deal with these color options are transitive--otherwise you'll just end up with the original, non-transformed values.