Transitive transforms appear to be happening out of order

471 Views Asked by At

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?

2

There are 2 best solutions below

0
On BEST ANSWER

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.
0
On

We had this exact same issue. This was happening because non-transitive transforms were occurring in our mix function, even if the matcher excluded that particular token.

What ended up working for us was to first console.log what was being used in the mix method and using Regex to parse it into a color format that would work.

We ended up splitting color transforms up into two separate transforms:

  • One that is non-transitive, and would output in the final format we needed but had a matcher that excluded all color-blended tokens (ex. UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1)
  • The other that is transitive transform targeting color-blended tokens, that would then take in each prop and transform those already transformed values into a color format supported by tinyColor.

By using tinyColor's isValid() method, we could validate what was being passed in or throw an error. This helped us debug but would also help future contributions that required color blending.