@baloise/ds-tokens 19.9.4 → 20.0.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ const basePxFontSize = 16;
2
+ const mode = 'Base';
3
+ const config = {
4
+ source: [`tokens/${mode}.tokens.json`],
5
+ platforms: {
6
+ css: {
7
+ transformGroup: 'css',
8
+ transforms: ['ds/css/name', 'ds/color/rgba', 'ds/size/round', 'ds/size/rem'],
9
+ basePxFontSize,
10
+ buildPath: 'dist/',
11
+ prefix: 'ds',
12
+ files: [
13
+ {
14
+ format: 'ds/css/variables-responsive',
15
+ destination: `css/${mode.toLowerCase()}.tokens.css`,
16
+ },
17
+ ],
18
+ options: {
19
+ outputReferences: true,
20
+ },
21
+ },
22
+ scss: {
23
+ transformGroup: 'scss',
24
+ transforms: ['ds/css/name', 'ds/color/rgba', 'ds/size/round', 'ds/size/rem'],
25
+ basePxFontSize,
26
+ buildPath: 'dist/',
27
+ prefix: 'ds',
28
+ files: [
29
+ {
30
+ format: 'scss/variables',
31
+ destination: `sass/${mode.toLowerCase()}.tokens.scss`,
32
+ },
33
+ ],
34
+ options: {
35
+ outputReferences: true,
36
+ },
37
+ },
38
+ web: {
39
+ transformGroup: 'web',
40
+ transforms: ['ds/css/name', 'ds/color/hex', 'ds/size/rem'],
41
+ prefix: 'ds',
42
+ buildPath: 'dist/',
43
+ files: [
44
+ {
45
+ format: 'json/flat',
46
+ destination: `web/${mode.toLowerCase()}.tokens.json`,
47
+ },
48
+ ],
49
+ options: {
50
+ outputReferences: true,
51
+ },
52
+ },
53
+ docs: {
54
+ transformGroup: 'web',
55
+ transforms: ['ds/css/name', 'ds/color/hex', 'ds/size/rem'],
56
+ prefix: 'ds',
57
+ buildPath: 'dist/',
58
+ files: [
59
+ {
60
+ format: 'json',
61
+ destination: `docs/${mode.toLowerCase()}.tokens.json`,
62
+ },
63
+ ],
64
+ options: {
65
+ outputReferences: true,
66
+ },
67
+ },
68
+ javascript: {
69
+ transformGroup: 'js',
70
+ transforms: ['ds/js/name', 'ds/color/hex', 'ds/size/round'],
71
+ prefix: 'ds',
72
+ buildPath: 'dist/',
73
+ files: [
74
+ {
75
+ format: 'javascript/es6',
76
+ destination: `js/${mode.toLowerCase()}.tokens.js`,
77
+ },
78
+ ],
79
+ options: {
80
+ outputReferences: true,
81
+ },
82
+ },
83
+ },
84
+ };
85
+ export default config;
@@ -0,0 +1,82 @@
1
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
2
+ const basePxFontSize = 16;
3
+ /**
4
+ * Recursively walks both token trees and returns only the tokens whose
5
+ * `$value` differs from the base. Keeps the full hierarchy so Style
6
+ * Dictionary can still resolve references.
7
+ */
8
+ function computeTokenDiff(base, brand) {
9
+ const result = {};
10
+ for (const [key, brandVal] of Object.entries(brand)) {
11
+ if (typeof brandVal !== 'object' || brandVal === null)
12
+ continue;
13
+ const baseVal = (base?.[key] ?? {});
14
+ if ('$value' in brandVal) {
15
+ // Token leaf — only keep it when the value actually changed
16
+ const brandSerialized = JSON.stringify(brandVal['$value']);
17
+ const baseSerialized = JSON.stringify(baseVal['$value']);
18
+ if (brandSerialized !== baseSerialized) {
19
+ result[key] = brandVal;
20
+ }
21
+ }
22
+ else {
23
+ // Group node — recurse and only include if something inside changed
24
+ const nested = computeTokenDiff(baseVal, brandVal);
25
+ if (Object.keys(nested).length > 0) {
26
+ result[key] = nested;
27
+ }
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+ /**
33
+ * Creates a Style Dictionary config for a brand override build.
34
+ *
35
+ * Computes the diff between Base and the brand token file so that only
36
+ * genuinely changed tokens end up in the output CSS. Works whether the brand
37
+ * file is a minimal override or a full Figma-mode export with all tokens.
38
+ *
39
+ * Returns the config and a `cleanup` function that removes the temporary diff
40
+ * file that was written to disk so Style Dictionary can mark it as `source`.
41
+ */
42
+ export function createBrandConfig(mode) {
43
+ const selector = `[data-theme="${mode.toLowerCase()}"]`;
44
+ const tmpFile = `tokens/.${mode.toLowerCase()}-diff.tmp.json`;
45
+ const baseJson = JSON.parse(readFileSync(`tokens/Base.tokens.json`, 'utf8'));
46
+ const brandJson = JSON.parse(readFileSync(`tokens/${mode}.tokens.json`, 'utf8'));
47
+ const diffTokens = computeTokenDiff(baseJson, brandJson);
48
+ // Write diff to a temp file — Style Dictionary only marks file-based source
49
+ // tokens as isSource:true, which is required for the brand formatter filter.
50
+ writeFileSync(tmpFile, JSON.stringify(diffTokens));
51
+ const config = {
52
+ include: [`tokens/Base.tokens.json`],
53
+ source: [tmpFile],
54
+ platforms: {
55
+ css: {
56
+ transforms: ['name/kebab', 'ds/css/name', 'ds/color/rgba', 'ds/size/round', 'ds/size/rem'],
57
+ basePxFontSize,
58
+ buildPath: 'dist/',
59
+ prefix: 'ds',
60
+ files: [
61
+ {
62
+ format: 'ds/css/variables-brand',
63
+ destination: `css/${mode.toLowerCase()}.tokens.css`,
64
+ options: {
65
+ selector,
66
+ outputReferences: true,
67
+ },
68
+ },
69
+ ],
70
+ },
71
+ },
72
+ };
73
+ const cleanup = () => {
74
+ try {
75
+ unlinkSync(tmpFile);
76
+ }
77
+ catch {
78
+ // ignore — file may already be gone
79
+ }
80
+ };
81
+ return { config, cleanup };
82
+ }
@@ -0,0 +1,236 @@
1
+ import { fileHeader, formattedVariables } from 'style-dictionary/utils';
2
+ import { propertyFormatNames } from 'style-dictionary/enums';
3
+ export const registerCustomFormatters = (sd) => {
4
+ /**
5
+ * CSS Responsive Formatter
6
+ * ------------------------------------------------------
7
+ */
8
+ sd.registerFormat({
9
+ name: 'ds/css/variables-responsive',
10
+ format: async ({ dictionary, file, options }) => {
11
+ const { outputReferences } = options;
12
+ const header = await fileHeader({ file });
13
+ // find reponsive tokens in dictionary which ends with -mobile, -tablet or -desktop
14
+ const baseTokensOriginal = dictionary.allTokens.filter(token => token.name.endsWith('-mobile'));
15
+ // create a deeop copy of base tokens
16
+ const baseTokens = JSON.parse(JSON.stringify(baseTokensOriginal));
17
+ const deviceBaseTokens = JSON.parse(JSON.stringify(baseTokensOriginal));
18
+ //
19
+ // Base tokens
20
+ // ------------------------------------------------------
21
+ // same as mobile but without the suffix
22
+ baseTokens.forEach(token => {
23
+ token.name = token.name.replace('-mobile', '');
24
+ });
25
+ const baseDictionary = {
26
+ ...dictionary,
27
+ allTokens: baseTokens,
28
+ };
29
+ //
30
+ // Device tokens
31
+ // ------------------------------------------------------
32
+ // same as mobile but without the suffix
33
+ deviceBaseTokens.forEach(token => {
34
+ token.name = token.name.replace('-mobile', '-device');
35
+ });
36
+ const deviceBaseDictionary = {
37
+ ...dictionary,
38
+ allTokens: deviceBaseTokens,
39
+ };
40
+ const tabletTokensOriginal = dictionary.allTokens.filter(token => token.name.endsWith('-tablet'));
41
+ const deviceTabletTokens = JSON.parse(JSON.stringify(tabletTokensOriginal));
42
+ deviceTabletTokens.forEach(token => {
43
+ token.name = token.name.replace('-tablet', '-device');
44
+ });
45
+ const deviceTabletDictionary = {
46
+ ...dictionary,
47
+ allTokens: deviceTabletTokens,
48
+ };
49
+ const desktopTokensOriginal = dictionary.allTokens.filter(token => token.name.endsWith('-desktop'));
50
+ const deviceDesktopTokens = JSON.parse(JSON.stringify(desktopTokensOriginal));
51
+ deviceDesktopTokens.forEach(token => {
52
+ token.name = token.name.replace('-desktop', '-device');
53
+ });
54
+ const deviceDesktopDictionary = {
55
+ ...dictionary,
56
+ allTokens: deviceDesktopTokens,
57
+ };
58
+ // const mobileCss =
59
+ // ':root {\n' +
60
+ // formattedVariables({
61
+ // format: propertyFormatNames.css,
62
+ // dictionary,
63
+ // outputReferences,
64
+ // usesDtcg: true,
65
+ // }) +
66
+ // '\n\n' +
67
+ // ' /* Base tokens */\n' +
68
+ // formattedVariables({
69
+ // format: propertyFormatNames.css,
70
+ // dictionary: baseDictionary,
71
+ // outputReferences,
72
+ // usesDtcg: true,
73
+ // }) +
74
+ // '\n\n' +
75
+ // ' /* Device tokens */\n' +
76
+ // formattedVariables({
77
+ // format: propertyFormatNames.css,
78
+ // dictionary: deviceBaseDictionary,
79
+ // outputReferences,
80
+ // usesDtcg: true,
81
+ // }) +
82
+ // '\n}\n\n' +
83
+ // '/* Device tokens: Tablet */\n' +
84
+ // `\n@media (min-width: 769px) {\n` +
85
+ // formattedVariables({
86
+ // format: propertyFormatNames.css,
87
+ // dictionary: deviceTabletDictionary,
88
+ // outputReferences,
89
+ // usesDtcg: true,
90
+ // }) +
91
+ // `\n}\n\n` +
92
+ // '/* Device tokens: Desktop */\n' +
93
+ // `\n@media (min-width: 1024px) {\n` +
94
+ // formattedVariables({
95
+ // format: propertyFormatNames.css,
96
+ // dictionary: deviceDesktopDictionary,
97
+ // outputReferences,
98
+ // usesDtcg: true,
99
+ // }) +
100
+ // `\n}\n` +
101
+ // '\n'
102
+ return (header +
103
+ ':root {\n' +
104
+ formattedVariables({
105
+ format: propertyFormatNames.css,
106
+ dictionary,
107
+ outputReferences,
108
+ usesDtcg: true,
109
+ }) +
110
+ '\n\n' +
111
+ ' /* Base tokens */\n' +
112
+ formattedVariables({
113
+ format: propertyFormatNames.css,
114
+ dictionary: baseDictionary,
115
+ outputReferences,
116
+ usesDtcg: true,
117
+ }) +
118
+ '\n\n' +
119
+ ' /* Device tokens */\n' +
120
+ formattedVariables({
121
+ format: propertyFormatNames.css,
122
+ dictionary: deviceBaseDictionary,
123
+ outputReferences,
124
+ usesDtcg: true,
125
+ }) +
126
+ '\n}\n\n' +
127
+ '/* Device tokens: Tablet */\n' +
128
+ `\n@media (min-width: 769px) {\n` +
129
+ ':root {\n' +
130
+ formattedVariables({
131
+ format: propertyFormatNames.css,
132
+ dictionary: deviceTabletDictionary,
133
+ outputReferences,
134
+ usesDtcg: true,
135
+ }) +
136
+ `\n}` +
137
+ `\n}\n\n` +
138
+ '/* Device tokens: Desktop */\n' +
139
+ `\n@media (min-width: 1024px) {\n` +
140
+ ':root {\n' +
141
+ formattedVariables({
142
+ format: propertyFormatNames.css,
143
+ dictionary: deviceDesktopDictionary,
144
+ outputReferences,
145
+ usesDtcg: true,
146
+ }) +
147
+ `\n}` +
148
+ `\n}\n` +
149
+ '\n');
150
+ },
151
+ });
152
+ /**
153
+ * CSS Brand Formatter
154
+ * ------------------------------------------------------
155
+ */
156
+ sd.registerFormat({
157
+ name: 'ds/css/variables-brand',
158
+ format: async ({ dictionary, file, options }) => {
159
+ const { outputReferences } = options;
160
+ const selector = options.selector ?? '[data-theme="brand"]';
161
+ const header = await fileHeader({ file });
162
+ // Only emit tokens that come from the brand source file, not from include (base)
163
+ const sourceTokens = dictionary.allTokens.filter(token => token.isSource);
164
+ const sourceDictionary = { ...dictionary, allTokens: sourceTokens };
165
+ const baseTokensOriginal = sourceTokens.filter(token => token.name.endsWith('-mobile'));
166
+ const baseTokens = JSON.parse(JSON.stringify(baseTokensOriginal));
167
+ const deviceBaseTokens = JSON.parse(JSON.stringify(baseTokensOriginal));
168
+ baseTokens.forEach(token => {
169
+ token.name = token.name.replace('-mobile', '');
170
+ });
171
+ deviceBaseTokens.forEach(token => {
172
+ token.name = token.name.replace('-mobile', '-device');
173
+ });
174
+ const baseDictionary = { ...dictionary, allTokens: baseTokens };
175
+ const deviceBaseDictionary = { ...dictionary, allTokens: deviceBaseTokens };
176
+ const tabletTokensOriginal = sourceTokens.filter(token => token.name.endsWith('-tablet'));
177
+ const deviceTabletTokens = JSON.parse(JSON.stringify(tabletTokensOriginal));
178
+ deviceTabletTokens.forEach(token => {
179
+ token.name = token.name.replace('-tablet', '-device');
180
+ });
181
+ const deviceTabletDictionary = { ...dictionary, allTokens: deviceTabletTokens };
182
+ const desktopTokensOriginal = sourceTokens.filter(token => token.name.endsWith('-desktop'));
183
+ const deviceDesktopTokens = JSON.parse(JSON.stringify(desktopTokensOriginal));
184
+ deviceDesktopTokens.forEach(token => {
185
+ token.name = token.name.replace('-desktop', '-device');
186
+ });
187
+ const deviceDesktopDictionary = { ...dictionary, allTokens: deviceDesktopTokens };
188
+ return (header +
189
+ `${selector} {\n` +
190
+ formattedVariables({
191
+ format: propertyFormatNames.css,
192
+ dictionary: sourceDictionary,
193
+ outputReferences,
194
+ usesDtcg: true,
195
+ }) +
196
+ '\n\n /* Base tokens */\n' +
197
+ formattedVariables({
198
+ format: propertyFormatNames.css,
199
+ dictionary: baseDictionary,
200
+ outputReferences,
201
+ usesDtcg: true,
202
+ }) +
203
+ '\n\n /* Device tokens */\n' +
204
+ formattedVariables({
205
+ format: propertyFormatNames.css,
206
+ dictionary: deviceBaseDictionary,
207
+ outputReferences,
208
+ usesDtcg: true,
209
+ }) +
210
+ '\n}\n\n' +
211
+ '/* Device tokens: Tablet */\n' +
212
+ `\n@media (min-width: 769px) {\n` +
213
+ `${selector} {\n` +
214
+ formattedVariables({
215
+ format: propertyFormatNames.css,
216
+ dictionary: deviceTabletDictionary,
217
+ outputReferences,
218
+ usesDtcg: true,
219
+ }) +
220
+ `\n}` +
221
+ `\n}\n\n` +
222
+ '/* Device tokens: Desktop */\n' +
223
+ `\n@media (min-width: 1024px) {\n` +
224
+ `${selector} {\n` +
225
+ formattedVariables({
226
+ format: propertyFormatNames.css,
227
+ dictionary: deviceDesktopDictionary,
228
+ outputReferences,
229
+ usesDtcg: true,
230
+ }) +
231
+ `\n}` +
232
+ `\n}\n` +
233
+ '\n');
234
+ },
235
+ });
236
+ };
@@ -0,0 +1,43 @@
1
+ import StyleDictionary from 'style-dictionary';
2
+ import { copy, ensureDir, pathExists } from 'fs-extra';
3
+ import { resolve } from 'path';
4
+ console.log(`
5
+ \x1b[35m┃\x1b[0m
6
+ \x1b[35m┃\x1b[0m \x1b[1;37m🧩 Helvetia Design System\x1b[0m
7
+ \x1b[35m┃\x1b[0m \x1b[90m🎨 Building Tokens Package\x1b[0m
8
+ \x1b[35m┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
9
+ `);
10
+ import { registerCustomTransformers } from './transformers.js';
11
+ import { registerCustomFormatters } from './formatter.js';
12
+ registerCustomTransformers(StyleDictionary);
13
+ registerCustomFormatters(StyleDictionary);
14
+ import ConfigBase from './config.base.js';
15
+ import { createBrandConfig } from './config.brand.js';
16
+ // Base build
17
+ const StyleDictionaryBase = new StyleDictionary(ConfigBase);
18
+ StyleDictionaryBase.buildAllPlatforms();
19
+ // Brand builds — add new brand names here (must match tokens/<Name>.tokens.json)
20
+ const brands = ['Tcs'];
21
+ for (const brand of brands) {
22
+ const { config, cleanup } = createBrandConfig(brand);
23
+ try {
24
+ const sd = new StyleDictionary(config);
25
+ await sd.buildAllPlatforms();
26
+ }
27
+ finally {
28
+ cleanup();
29
+ }
30
+ }
31
+ // copy generated files to css folder in core assets
32
+ const projectRoot = process.cwd();
33
+ const sourceDir = resolve(projectRoot, 'dist', 'css');
34
+ const targetDir = resolve(projectRoot, '..', 'core', 'www', 'assets', 'tokens');
35
+ (async () => {
36
+ await ensureDir(targetDir);
37
+ if (await pathExists(sourceDir)) {
38
+ await copy(sourceDir, targetDir, { overwrite: true });
39
+ }
40
+ else {
41
+ console.warn(`Tokens CSS directory not found at: ${sourceDir}`);
42
+ }
43
+ })();
@@ -0,0 +1,150 @@
1
+ export const registerCustomTransformers = (sd) => {
2
+ /**
3
+ * Transform color tokens with hex and alpha properties to rgba() format
4
+ */
5
+ sd.registerTransform({
6
+ type: `value`,
7
+ transitive: true,
8
+ name: `ds/color/rgba`,
9
+ filter: token => token.$type === 'color',
10
+ transform: token => {
11
+ const value = token.$value ?? token.value;
12
+ // Handle object values with hex and alpha properties
13
+ if (typeof value === 'object' && value !== null && 'hex' in value && 'alpha' in value && value.alpha < 1) {
14
+ const hex = value.hex.replace('#', '');
15
+ const r = parseInt(hex.substring(0, 2), 16);
16
+ const g = parseInt(hex.substring(2, 4), 16);
17
+ const b = parseInt(hex.substring(4, 6), 16);
18
+ const a = parseFloat(value.alpha);
19
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
20
+ }
21
+ if (value.hex) {
22
+ return value.hex;
23
+ }
24
+ return value;
25
+ },
26
+ });
27
+ /**
28
+ * Transform color tokens with hex
29
+ */
30
+ sd.registerTransform({
31
+ type: `value`,
32
+ transitive: true,
33
+ name: `ds/color/hex`,
34
+ filter: token => token.$type === 'color',
35
+ transform: token => {
36
+ const value = token.$value ?? token.value;
37
+ // Handle object values with hex and alpha properties
38
+ if (typeof value === 'object' && value !== null && 'hex' in value && 'alpha' in value) {
39
+ return value.hex;
40
+ }
41
+ return value;
42
+ },
43
+ });
44
+ /**
45
+ * Transform token names for CSS usage
46
+ */
47
+ sd.registerTransform({
48
+ type: `name`,
49
+ transitive: true,
50
+ name: `ds/css/name`,
51
+ transform: token => {
52
+ let tokenName = token.name;
53
+ const isComponent = token.path.includes('🧩 Component');
54
+ if (isComponent) {
55
+ tokenName = tokenName.replace('-component', '');
56
+ }
57
+ // we use t-shirt sizes and need to make sure that 2xl becomes 2xl and not 2-xl
58
+ // check if token names has number followed by xl or xs and replace it with numberxl without dash
59
+ if (/-?([0-9]+)-(xl|xs)/.test(tokenName)) {
60
+ tokenName = tokenName.replace(/-?([0-9]+)-(xl|xs)/, '-$1$2');
61
+ }
62
+ // check that no name has double dash and replace it with single dash
63
+ if (tokenName.includes('--')) {
64
+ tokenName = tokenName.replace(/--+/g, '-');
65
+ }
66
+ return tokenName;
67
+ },
68
+ });
69
+ /**
70
+ * Transform token names for CSS usage
71
+ */
72
+ sd.registerTransform({
73
+ type: `name`,
74
+ transitive: true,
75
+ name: `ds/js/name`,
76
+ transform: token => {
77
+ let tokenName = token.name;
78
+ tokenName = tokenName.replace('Primitive', 'Global');
79
+ tokenName = tokenName.replace('Semantic', 'Alias');
80
+ return tokenName;
81
+ },
82
+ });
83
+ /**
84
+ * Transform size tokens from px to rem
85
+ */
86
+ sd.registerTransform({
87
+ type: `value`,
88
+ transitive: true,
89
+ name: `ds/size/rem`,
90
+ filter: token => token.$type === 'number',
91
+ transform: token => {
92
+ // const name = token.name
93
+ const value = token.$value;
94
+ // const originalValue = token.original.$value
95
+ const path = token.path;
96
+ // const isReference =
97
+ // typeof originalValue === 'string' && originalValue.startsWith('{') && originalValue.endsWith('}')
98
+ if (`${value}`.endsWith('px')) {
99
+ return value;
100
+ }
101
+ if (`${value}`.endsWith('rem')) {
102
+ return value;
103
+ }
104
+ // Number only values with no unit
105
+ const tokenToBeNumberOnly = [
106
+ 'LineHeight',
107
+ 'FontWeight',
108
+ 'Opacity',
109
+ '🌫️ Opacity',
110
+ 'Z-Index',
111
+ '🗂️ Z-Index',
112
+ 'Interaction',
113
+ '✨ Interaction',
114
+ ];
115
+ if (tokenToBeNumberOnly.some(ignored => path.includes(ignored))) {
116
+ return Math.round(value * 10) / 10;
117
+ }
118
+ // Number only values with no unit
119
+ const tokenToBeNumberPixel = ['📐 Breakpoint', 'Breakpoint', '🗃️ Container', 'Container'];
120
+ if (tokenToBeNumberPixel.some(ignored => path.includes(ignored))) {
121
+ return value + 'px';
122
+ }
123
+ // Extra case for rounded radius
124
+ if (value === 9999) {
125
+ return value + 'px';
126
+ }
127
+ // turn pixel into rem
128
+ return value / 16 + 'rem';
129
+ },
130
+ });
131
+ sd.registerTransform({
132
+ type: `value`,
133
+ transitive: true,
134
+ name: `ds/size/round`,
135
+ filter: token => token.$type === 'number',
136
+ transform: token => {
137
+ const value = token.$value ?? token.value;
138
+ const name = token.name;
139
+ const tokenName = Array.isArray(name) ? name.join('-') : String(name ?? '');
140
+ // if line-height round the value to 1 decimal place
141
+ if (tokenName.includes('line-height') ||
142
+ tokenName.includes('opacity') ||
143
+ tokenName.includes('LineHeight') ||
144
+ tokenName.includes('Opacity')) {
145
+ return Math.round(value * 10) / 10;
146
+ }
147
+ return value;
148
+ },
149
+ });
150
+ };