-
Notifications
You must be signed in to change notification settings - Fork 557
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
Support Composite Tokens #848
Comments
@wraybowling I am struggling with the same issue as well and thought there was some workaround, but do I understand you correctly that this is simply just not possible at the moment? |
I got it working using a custom transformer, that I called const StyleDictionary = require('style-dictionary');
StyleDictionary.registerTransform({
type: 'value',
transitive: true,
name: 'figma/web/flatten-properties',
matcher: ({ type }) => {
return ['typography', 'composition'].includes(type);
},
transformer: ({ value, name, type }) => {
if (!value) return;
const entries = Object.entries(value);
const flattendedValue = entries.reduce(
(acc, [key, v], index) =>
`${acc ? `${acc}\n ` : ''}--${name}-${StyleDictionary.transform['name/cti/kebab'].transformer(
{ path: [key] },
{ prefix: '' },
)}: ${v}${index + 1 === entries.length ? '' : ';'}`,
`${name.includes(type) ? '' : `${type}-`}${name}-group;`,
);
return flattendedValue;
},
}); I hope it solves your issue :) |
I appreciate seeing a workaround in the thread, but this issue should not be closed until Style Dictionary can handle the w3c spec without it being something custom. Perhaps we could use your transformer as the starting point for the new default. |
Hey, we've experienced the same issue, so I'll just chime in with our findings / solution: When outputting typography css classes, it's really useful to have all the properties for a given typography grouped under .heading-level-1 {
font-family: "Some Brand Font Face";
font-weight: 300;
line-height: 48px;
font-size: 24px;
letter-spacing: 0em;
text-decoration: none;
text-transform: none;
} On top of that it makes sense to group:
as mentioned in the draft from the W3C Design Tokens Community Group on Composite Tokens in general as well as the specific types for typography. BUT: When outputting this structure as flattened tokens (e.g. as css or scss) we get the $heading-level-1: [object Object];
...
$tokens: (
...
'heading': (
'level-1': $heading-level-1
)
),
...
); Or solution is to:
const expandToken = (
token: TransformedToken,
nameTransformer: (token: TransformedToken) => string
): TransformedToken | TransformedToken[] => {
if (typeof token.value !== "object") {
return token;
}
const { attributes, name, path, value: _value, ...rest } = token;
return Object.entries(token.value).map(([key, value]) => {
const childPath = [...path, key];
var childName = nameTransformer({
...token,
path: childPath,
});
return {
...rest,
...(attributes ? { attributes: { ...attributes, subitem: key } } : {}),
name: childName,
path: childPath,
value,
};
});
};
// Create a shallow copy - we'll create new tokens in `allTokens|allProperties` when expanding composite tokens below:
const expandedDictionary = { ...dictionary };
// Expand composite tokens
// Note: we need to overwrite both `allTokens` and `allProperties` as long as the latter deprecated alias exists
// See: https://amzn.github.io/style-dictionary/#/version_3?id=style-properties-%e2%86%92-design-tokens
expandedDictionary.allTokens = expandedDictionary.allProperties =
dictionary.allTokens
.map((token) => expandToken(token, nameTransformer))
.flat(); This produces the following output (continuing with the example): $heading-level-1-font-family: "Some Brand Font Face";
$heading-level-1-font-weight: 300;
$heading-level-1-line-height: 48px;
$heading-level-1-font-size: 24px;
$heading-level-1-letter-spacing: 0em;
$heading-level-1-paragraph-spacing: 0,
$heading-level-1-text-decoration: none;
$heading-level-1-text-case: none;
...
$tokens: (
...
'heading': (
'level-1': (
'fontFamily': $heading-level-1-font-family,
'fontWeight': $heading-level-1-font-weight,
'lineHeight': $heading-level-1-line-height,
'fontSize': $heading-level-1-font-size,
'letterSpacing': $heading-level-1-letter-spacing,
'paragraphSpacing': $heading-level-1-paragraph-spacing,
'textDecoration': $heading-level-1-text-decoration,
'textCase': $heading-level-1-text-case
)
),
...
); This also solves the
if (typeof obj['value'] === 'object') {
// if we have found a composite group of child values, use the Sass group "(...)" syntax and loop on the children:
var compositeProp = obj['value'];
output += '(\n'
output += Object.keys(compositeProp).map(function(subKey) {
var indent = ' '.repeat(depth+1);
var subvalueName = nameTransformer({...obj, path: [...obj.path, subKey]});
return `${indent}'${subKey}': $${subvalueName}`;
}).join(',\n');
output += '\n' + ' '.repeat(depth) + ')';
} else {
// if we have found a leaf (a property with a value) append the value
output += `$${obj.name}`;
} For option 2 to work we have to enhance the Might need @dbanksdesign to chime in here (nudge-nudge 😉) for any thoughts on that part - as well as him being part of the W3C Design Tokens Community Group and might elaborate as to wether this approach is fit with their current thinking on composite tokens. Jakes 😊 |
First, apologies for being a bit MIA (personal life has been extraordinarily busy the past few months). To start, the core of Style Dictionary does support composite tokens, but as you all know the pre-built transforms and formats do not understand how to deal with composite tokens. Here is an example which uses color tokens as composites of hue, saturation, and lightness: https://github.com/amzn/style-dictionary/tree/main/examples/advanced/transitive-transforms Style Dictionary supports composite tokens in its core architecture by how tokens get identified, transformed, and resolved. I created this quick example to show that transforms (at least for non-composite tokens used inside composite tokens) and resolutions work. https://stackblitz.com/edit/style-dictionary-example-vkhiqj?file=build/test.json The reason you see The difficulty with composite tokens is there is usually more than one way to output them. In this issue there are 2 different potential correct outputs: splitting the token up into separate variables (like the SCSS example), and outputting a CSS helper class like @wraybowling could you fill in what your expected output would look like? The other difficulty with composite tokens is they require a specific structure to the value. Non-composite tokens are just strings or numbers, but now a composite token's value structure or type is dependent on the composite type (border has a size, color, and style). The W3C spec is still looking for feedback on the structure of these composite types, for example: design-tokens/community-group#102 I think this is definitely something we should support this in some way in version 4. |
I was intrigued by jakobe's idea to split composite tokens into individual tokens. Worked out a similar solution using a custom parser (stackblitz), which works pretty good. The one thing I can't settle on is whether to persist the composite token after splitting it up. Keeping it around makes it easy to achieve the following: --border-thin-width: 1px;
--border-thin-style: solid;
--border-thin-color: rebeccapurple;
--border-thin: 1px solid rebeccapurple; /* note, for this to work, the token's path is `border.thin.@` */
--card-border: 1px solid rebeccapurple; /* alias! `card-border: { value: "{border.thin.@}" }` */ Though, that pattern may only work well for composite tokens that map to a CSS shorthand property. For example, the typography composite token includes a Furthermore, aliasing wouldn't be possible without persisting the composite token. |
You all know about Lukas Oppermann's repo, right? https://github.com/lukasoppermann/style-dictionary-utils |
<!-- How to write a good PR title: - Follow [the Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). - Give as much context as necessary and as little as possible - Prefix it with [WIP] while it’s a work in progress --> ## Self Checklist - [x] I wrote a PR title in **English** and added an appropriate **label** to the PR. - [x] I wrote the commit message in **English** and to follow [**the Conventional Commits specification**](https://www.conventionalcommits.org/en/v1.0.0/). - [x] I [added the **changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md) about the changes that needed to be released. (or didn't have to) - [x] I wrote or updated **documentation** related to the changes. (or didn't have to) - [x] I wrote or updated **tests** related to the changes. (or didn't have to) - [x] I tested the changes in various browsers. (or didn't have to) - Windows: Chrome, Edge, (Optional) Firefox - macOS: Chrome, Edge, Safari, (Optional) Firefox ## Related Issue <!-- Please link to issue if one exists --> Fixes #994 ## Summary <!-- Please brief explanation of the changes made --> 베지어 디자인 시스템의 디자인 토큰 패키지인 bezier-tokens 패키지를 추가합니다. > **디자인 토큰이 무엇인가요?** > > Design tokens are a methodology for expressing design decisions in a platform-agnostic way so that they can be shared across different disciplines, tools, and technologies. They help establish a common vocabulary across organisations. (from w3c dtcg) > > - https://design-tokens.github.io/community-group/format/ > - https://m3.material.io/foundations/design-tokens/overview ## Details <!-- Please elaborate description of the changes --> ### Note - 여러 디자인 토큰 변환 라이브러리를 리서치해보았습니다. 사용자의 규모와 향후 업데이트 로드맵, 커스터마이즈 가능 범위 등을 살펴보았을 때, Style dictionary가 가장 적절하다고 생각하여 선택하였습니다. - 디자인 토큰을 피그마에서 연동하기에는 현상황에서 어려웠습니다. 현재 피그마 Variables가 오픈베타여서 타이포그래피 등의 토큰 등을 지원하고 있지 않는 상황입니다. 또한 피그마 Variables의 등장으로 Token Studio같은 서드파티 플러그인을 사용하지 않기로 팀 내부에서 결정했기 때문에, 피그마 Variables의 스펙이 언제든지 추가되거나 변할 수 있다는 뜻입니다. 따라서 지금 피그마-소스 코드 변환기를 구현하는 건 시기상조라고 생각했습니다. - 현재 작업중인 새로운 디자인 시스템에 토큰을 적용하지 않고, 기존의(프로덕션) 레거시 디자인 토큰을 적용했습니다. 정확히는 현재 bezier-react의 Foundation들을 디자인 토큰으로 분해했습니다(= 피그마에는 토큰으로 분류되지 않은 경우도 있습니다). 토큰 적용 & 정적 스타일링 방식으로 변경 -> 새로운 디자인 토큰 적용으로 단계를 나누어가기 위해서입니다. ### Build step 빌드는 간략하게 다음의 과정으로 이루어집니다. 1. JSON(Design token)을 cjs/esm/css 로 변환합니다. 2. 변환된 cjs/esm 의 엔트리포인트(index.js)를 만듭니다. 3. 타입스크립트 컴파일러를 통해 변환된 js 파일로부터 타입 선언을 만듭니다. - **향후 1번의 변환 과정에 iOS, Android용 스타일 변환기, JSON 변환기 등을 추가할 수 있습니다.** - 1번의 변환 과정은 글로벌 토큰(기존의 팔레트, 레디우스 등)과 시맨틱 토큰(라이트/다크 테마)이 별개로 이루어집니다. 라이트/다크 테마를 함께 빌드하게 되면 키가 충돌했다는 메세지와 함께 빌드 에러가 발생합니다. themeable같은 속성을 사용할 수도 있으나, JSON에 style-dictionary 라이브러리에 종속적인 속성을 포함시키고 싶지 않았습니다. 토큰은 더 순수하게 두는 게 나중을 위하여 좋다고 판단했습니다. - Composite token(예: 타이포그래피)를 지원하지 않습니다. 현재 공식적으로 지원하지 않는 스펙이며, 현상황에서는 개별 토큰들을 bezier-react(그 외 각 플랫폼 디자인 시스템)에서 조합해도 큰 무리가 없다고 판단했습니다. #### File tree ```md dist ┣ cjs ┃ ┣ darkTheme.js ┃ ┣ global.js ┃ ┣ index.js ┃ ┗ lightTheme.js ┣ css ┃ ┣ dark-theme.css ┃ ┣ global.css ┃ ┗ light-theme.css ┣ esm ┃ ┣ darkTheme.mjs ┃ ┣ global.mjs ┃ ┣ index.mjs ┃ ┗ lightTheme.mjs ┗ types ┃ ┣ cjs ┃ ┃ ┣ darkTheme.d.ts ┃ ┃ ┣ darkTheme.d.ts.map ┃ ┃ ┣ global.d.ts ┃ ┃ ┣ global.d.ts.map ┃ ┃ ┣ index.d.ts ┃ ┃ ┣ index.d.ts.map ┃ ┃ ┣ lightTheme.d.ts ┃ ┃ ┗ lightTheme.d.ts.map ┃ ┗ esm ┃ ┃ ┣ darkTheme.d.mts ┃ ┃ ┣ darkTheme.d.mts.map ┃ ┃ ┣ global.d.mts ┃ ┃ ┣ global.d.mts.map ┃ ┃ ┣ index.d.mts ┃ ┃ ┣ index.d.mts.map ┃ ┃ ┣ lightTheme.d.mts ┃ ┃ ┗ lightTheme.d.mts.map ``` ### Next - 이 패키지의 js, css를 가지고 bezier-react의 스타일 시스템, 테마 기능을 구성하게 됩니다. (#1690) - 이 패키지의 토큰에 더해 bezier-react의 constants(disabled 0.4, z-index), 타이포그래피 등을 bezier-react에서 추가, 확장하여 최종적으로 사용자 애플리케이션에 제공하는 방향으로 구현하고자 합니다. (#1495 에서 작업) ### Breaking change? (Yes/No) <!-- If Yes, please describe the impact and migration path for users --> No ## References <!-- Please list any other resources or points the reviewer should be aware of --> - https://amzn.github.io/style-dictionary - https://dbanks.design/blog/dark-mode-with-style-dictionary/ - amzn/style-dictionary#848 : Composite token 관련 이슈
@jakobe I really like this approach - at what stage are you implementing this in order to mutate the dictionary prior to passing to the formatter? Thanks |
To summarize, there are 3 solutions in this thread:
Obviously all three are great ways to tackle this, but I think the first option should be something that Style Dictionary should come out of the box with as an opt-in, which means you as a user can still opt for the other alternatives. It might be cool to know that both sd-transforms and style-dictionary-utils have a transform for the third option (CSS shorthand) Fortunately, I created an expand composites utility in sd-transforms already because Tokens Studio has had composite type tokens for a while now, and this supports many edge cases such as references inside such tokens, cross-file references, etc. It wouldn't be much work to add this feature into Style Dictionary itself, it has 100% test coverage and is mostly re-using Style Dictionary utilities already, making it a good fit. Suggestion for API: {
"source": ["tokens.json"],
"expand": {
// only expand for typography / run function for typography tokens
"include": { "typography": true }, // or Function
// expand for all composite types except for typography, there we don't expand or we run function to check
"exclude": { "typography": true }, // or Function
// not specifying either exclude or include means we run expand on all tokens, analogous to "expand": true
// except this way allows you to specify a typesMap
"typesMap": {
"border": {
"width": "borderWidth"
}
}
}
} Where @dbanksdesign what do you think? |
@jorenbroekema Agreed, in an ideal world I feel we should be able to control this at a platform level, for example, I want to keep my composite token format for Figma and Web, as they both handle Typography tokens (in Figma, and with CSS Shorthand). However for Android and iOS I may want to expand composites as we don't have the option for certain shorthand properties, so need to be handled individually. My thoughts are that this should be controlled at the format level, but almost like a pre format stage, where we can expand the composite tokens into a group, for example, and pass the updated dictionary back so it can then go through the relevant template, e.g Android. I'd be interested to know your thoughts, or if anyone has taken this approach? |
Great points @lfantom , in v4 we have something called preprocessors which allows processing the dictionary object after the parsing step, this happens on a global level before any transforms are done, which is platform specific. I'm considering that perhaps we need a postprocessors hook that allows you to do the exact same but after transforms, so it's platform specific. I also discussed this with Danny in a private message and he seemed to agree with this concept. This means that the expandTokens utility can be done on a preprocessor (global) OR postprocessor (platform-specific) level. Thoughts? |
@jorenbroekema Yes I think the idea of adding a post-proccessors options is a good one - I've been testing out an implementation where this step is added after the transforms have taken place, and that seems to work well, as I've found it's necessary for the references to be resolved prior to expanding the tokens. One issue that I have found with this however, is when expanding say typography tokens, I'm able to assign the new type to each new token e.g fontSize is expanded into a single token and given the relevant type I hope that makes sense, I'd be interested to hear your thoughts on how to handle this? The only option I could see, was to add the additional attribute structure at the point of expanding the composite tokens, but that feels like the wrong place for it. Or maybe you could use your suggested api structure with |
@lfantom can you show a small example of what you mean with the attributes structure not being passed correctly when expanding a token in a postprocessor hook? I was thinking that for this expand postprocessor, we could run the |
@lukasoppermann I think I arrived at a somewhat elegant API now #848 (comment) |
Hey @jorenbroekema you can use I don't quite get it. Would this be valid? {
"source": ["border.json"],
"expand": {
"include": { "border": true }, // or Function
"typesMap": {
"border": {
"width": "borderWidth"
}
}
}
} What about this? {
"source": ["border.json"],
"expand": {
"typesMap": {
"border": {
"width": "borderWidth"
}
}
}
} Couldn't you just do an array instead for include? {
"source": ["border.json"],
"expand": {
"include": [ "border", fnExpandTypography], // fnExpandTypography would return typography or undefined
"typesMap": {
"border": {
"width": "borderWidth"
}
}
}
} I think I still prefer this: {
"source": ["border.json"],
"expand": {
"border": {
"width": "borderWidth"
},
"typography": true,
"shadow": expandShadow // fn
}
}
} const expandShadow = (token, platform) => { // idk which arguments it would get
if(!condition) return false // maybe undefined could also work
return {
"blur": "dimension"
}
} |
Oh yeah true thanks for the tip.
Yes, that would only expand border tokens, or if it's a function it will run the function for each border token to determine per token if it should be expanded.
Yep, that would expand all tokens, similar to
Yes possibly, perhaps we should consider that when you use a Function, you have access to the token.type, so it doesn't really make sense to have functions wrapped in composite types keys, so maybe this: {
"source": ["border.json"],
"expand": {
"include": [ "border", "typography"],
// OR (not in JSON but in JS in this case)
"include": (token, config, platformCfg) => true,
"typesMap": {
"border": {
"width": "borderWidth"
}
}
}
} and then for exclude it would be the same API except reverse effect: {
"source": ["border.json"],
"expand": {
"exclude": [ "border", "typography"],
// OR (not in JSON but in JS in this case)
"exclude": (token, config, platformCfg) => true, // true means it will get excluded, false means included
"typesMap": {
"border": {
"width": "borderWidth"
}
}
}
}
I just think that it's a bit awkward that it's either |
Support for expanding composite type tokens on the preprocessor level (either globally or per platform) was released in prerelease.27: https://v4.styledictionary.com/reference/config/#expand In the next prerelease (28), if such composite tokens are not expanded into separate tokens, there will now be built-in transforms for CSS (also included in the css, scss and less transformGroups by default) that will transform these object-value tokens into CSS shorthands, but keep in mind that not every CSS shorthand supports every single composite type token property (e.g. typography -> letterSpacing) |
@jorenbroekema Is there a resolution for the letterSpacing thing or should it just be ignored? I'm getting the following warnings after running SD:
I'm not using the letterSpacing fields in Figma Tokens Studio and tokens build correctly so it's not a big deal, just annoying. |
Style Dictionary disagrees apparently, can you show me the You can turn it off btw: https://v4.styledictionary.com/reference/logging/ (warnings -> 'disabled') |
Oh, I'm mistaken. I am using that field.
However
|
Yeah so if you're using properties that don't fit into the CSS shorthand, it's best to expand such object-value tokens into separate tokens for each property: https://v4.styledictionary.com/reference/config/#expand Also make sure to give this a read if you're using Tokens Studio / sd-transforms: https://github.com/tokens-studio/sd-transforms?tab=readme-ov-file#using-expand |
Thanks for the info, @jorenbroekema, but when I add the
and points to Here's my config:
This is the related section of my tokens.json file:
Everything built without issue prior to adding the |
Any idea what might be going on here, @jorenbroekema? Let me know if you'd prefer I create a new issue for this. |
@mikeriley131 This looks to be related to |
@thomasmattheussen could you clarify what you mean by "related to"? |
Well, I see that your error says something about |
Yeah so the problem is that currently in v4.0.0 the custom preprocessor (excludeParentKeys) runs AFTER the expand utility. This means that when the expand utility is doing its thing, references are "broken" because excludeParentKeys util hasn't ran yet. This is fixed in #1268 which will be in the next release. I'll try to get that out today. |
Just upgraded to v4.0.1 and my issues are resolved. Thanks! |
Lately I have been bumping into Style Dictionary's lack of support for composite tokens. At the time of this writing, composite tokens are part of the w3c spec draft, most notably for typography https://second-editors-draft.tr.designtokens.org/format/#typography
As an example of a tool making use of the spec, Figma Tokens is already providing composite tokens for typography. The token-transformer utility then outputs composite tokens. Finally, style dictionary is the weak link in making use of them, outputting scss variables as
[object Object]
see related issue: tokens-studio/figma-plugin#1088 (comment)
Details
Consider the following section from the attached tokens.json file which came from the Figma Tokens plugin
When the above is passed through token-transformer, and then Style Dictionary, the following SCSS (abridged) is produced:
To Reproduce
Steps to reproduce the behavior:
Alternatively, download my test project zip file and run
npm run build
Expected behavior
I expect to see on-spec typography tokens converted into valid SCSS variables
The text was updated successfully, but these errors were encountered: