Open jorenbroekema opened 11 months ago
Maybe the prop can be called deferred: true|false
btw, i think that's better than postTransitive
Here's another example that showcases the need to defer transformation until after all references are resolved when trying to build a transform that wraps math expressions inside calc()
.
{
"dimension": {
"scale": {
"value": "2",
"type": "sizing"
},
"xs": {
"value": "4px",
"type": "sizing"
},
"sm": {
"value": "{dimension.xs} * {dimension.scale}",
"type": "sizing"
},
"md": {
"value": "{dimension.sm} * {dimension.scale}",
"type": "sizing"
},
"lg": {
"value": "{dimension.md} * {dimension.scale}",
"type": "sizing"
},
}
}
StyleDictionary.registerTransform({
type: `value`,
transitive: true,
name: `figma/calc`,
matcher: ({ value }) => typeof value === 'string' && value.includes('*') && !value.includes('calc('),
transformer: ({ value }) => `calc(${value})`,
});
Expected output:
:root {
--sd-dimension-scale: 2;
--sd-dimension-xs: 4;
--sd-dimension-sm: calc(4px * 2);
--sd-dimension-md: calc(4px * 2 * 2);
--sd-dimension-lg: calc(4px * 2 * 2 * 2);
}
Actual output:
:root {
--sd-dimension-scale: 2px;
--sd-dimension-xs: 4px;
--sd-dimension-sm: calc(4px * 2px);
--sd-dimension-md: calc(4px * 2px) * 2px;
--sd-dimension-lg: calc(4px * 2px) * 2px * 2px;
}
Which is easy to explain when you understand the lifecycle of transitive transforms:
4px * 2
calc(4px * 2)
calc(4px * 2) * 2
There's a solution where we can always apply the transform even if it already has a calc statement, after which we get:
:root {
--sd-dimension-scale: 2;
--sd-dimension-xs: 4px;
--sd-dimension-sm: calc(4px * 2);
--sd-dimension-md: calc(calc(4px * 2) * 2);
--sd-dimension-lg: calc(calc(calc(4px * 2) * 2) * 2);
}
Which is actually valid CSS, but it just contains a nested calc statement for every chain of reference, which is a bit bloated..
When we allow this transform to be deferred at the end, we can get the expected outcome instead, which I think is the best solution.
I'd like to propose adding a property to transforms that indicates they ought to be ran after references have been resolved entirely.
This means that transforms can be ran in 3 different ways (in chronological order), rather than only 2:
true
, which means that the transform puts properties with references in a deferred state, after which goes into a repetitive cycle of resolving the reference for such properties and attempting to transform it once again. If the resolved value is another chained reference, this cycle continues until the resolved value is not a reference, after which the transitive transform is finally applied to it. See example below.true
which means this transform is ran at the very end when all references have been resolved and all regular and transitive transforms have done their jobs.Example transitive transform
Imagine the tokens below and a transitive transform that transforms the token value by a darken value, to darken the color.
In precise detail, events happen in the following order
["color.danger"]
.["color.danger", "color.error"]
.["color.danger", "color.error"]
are added to exclusion list for referencing, so any value that is a reference to these, we need to hold off because we first need to apply transitive transforms to our first layer of refs, a reference to{color.red}
for example.{color.red}
is resolved to#f00
, butcolor.error.value
which has{color.danger}
is skipped for now, because that one is in the ignorelist.color.danger
value is not a reference anymore. the original value is, so we only apply transitive transforms, meaning the value is darkened by0.75
.["color.error"]
.["color.error"]
, we do another resolve references call but this time the ignorelist only containscolor.error
becausecolor.danger
does not use a reference any longer.color.danger
withincolor.error
is resolved to whatever is the current value ofcolor.danger
, which is the#00
but by now it is darkened by0.75
.color.error
value is not a reference anymore. the original value is, so we only apply transitive transforms, meaning the value is darkened by0.5
.Example post-transitive transform
Input is the same:
However, the output is different, we now want:
Which means we have the transitive transform for the darken color modifier running, but we also have a transform running that needs to transform the format into swift UIColor format.
The problem is that we cannot at this time add a transform that runs after the color modifications have ran, so what happens now is:
color.danger
's reference tocolor.red
is resolved, but at this point this isUIColor(red: 255, green: 0, blue: 0, alpha: 1)
color.danger
but this color modifier doesn't understand UIColor format :( fatal error!color.error
's reference tocolor.danger
would be skipped due to ignorelist, would only happen in the next iteration if step 5 didn't fail. And this would then fail for the same reason.Hopefully this makes clear the use case for having post-transitive transforms.