@carbon/themes 11.78.0 → 11.79.0

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,157 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ *
12
+ * DTCG source files (g10.json, g90.json, g100.json) contain nodes that are
13
+ * simultaneously token leaves (they have a `$value`) AND token groups (they
14
+ * have non-`$`-prefixed children). Style Dictionary v5 does not support this
15
+ * pattern — when a node has `$value`, SD treats it as a leaf token and silently
16
+ * drops any non-`$` children.
17
+ *
18
+ * Example (g10.json):
19
+ *
20
+ * background: {
21
+ * $type: "color",
22
+ * $value: "{gray.10}", ← token leaf value
23
+ * active: { $type: "color", $value: "{gray.50}", ... }, ← child token
24
+ * brand: { ... }
25
+ * }
26
+ *
27
+ * Without this preprocessor, SD emits `background` but loses `background-active`
28
+ * and `background-brand`. There are 27 such dual-role nodes in each of
29
+ * g10 / g90 / g100, causing 48 tokens to go missing from the generated output.
30
+ *
31
+ * ── Solution ────────────────────────────────────────────────────────────────
32
+ *
33
+ * For any dual-role node (has `$value` AND non-`$` children), this preprocessor
34
+ * splits the node:
35
+ *
36
+ * 1. The parent's token value (`$value`, `$type`, `$extensions`) is moved into
37
+ * a synthetic `_self` child at the same level.
38
+ * 2. The parent node becomes a pure group (its `$value` is removed).
39
+ *
40
+ * After this transform, the tree becomes:
41
+ *
42
+ * background: {
43
+ * _self: { $type: "color", $value: "{gray.10}" }, ← parent value
44
+ * active: { $type: "color", $value: "{gray.50}", ... },
45
+ * brand: { ... }
46
+ * }
47
+ *
48
+ * SD sees `background._self` as path `['background', '_self']` and
49
+ * `background.active` as path `['background', 'active']`.
50
+ *
51
+ * A companion custom name transform (`carbon/name-kebab`, registered in
52
+ * sd.config.js) strips `_self` from the path when building the token name:
53
+ *
54
+ * ['background', '_self'] → 'background' (parent token)
55
+ * ['background', 'active'] → 'background-active'
56
+ * ['layer', '_self'] → 'layer'
57
+ * ['layer', '01'] → 'layer-01'
58
+ *
59
+ * This reproduces the flat token names that the old dtcg-converter produced,
60
+ * while preserving correct hierarchy for SD's alias resolution and path-based
61
+ * name computation.
62
+ *
63
+ * ── Alias resolution ────────────────────────────────────────────────────────
64
+ *
65
+ * The color-palette.json file is loaded as `source:` (not pre-processed) by
66
+ * sd.config.js, so its tokens stay in their original nested form and SD alias
67
+ * resolution ({blue.60} etc.) continues to work.
68
+ *
69
+ * This preprocessor is called with only the theme JSON as input (passed as
70
+ * `tokens:` in themeConfig()), never seeing the merged palette+theme tree.
71
+ * Therefore palette tokens are unaffected.
72
+ *
73
+ * @param {object} dictionary Raw DTCG token tree (theme file only)
74
+ * @returns {object} Normalised tree with dual-role nodes split
75
+ */
76
+
77
+ /**
78
+ * Process a single DTCG token node.
79
+ *
80
+ * The node may be:
81
+ * - A pure leaf token (has `$value`, no non-`$` children) → returned as-is.
82
+ * - A pure group (no `$value`, has non-`$` children) → children are recursed.
83
+ * - A dual-role node (has BOTH `$value` AND non-`$` children) → split: the
84
+ * `$value`/`$type`/`$extensions` are moved to a `_self` synthetic leaf, and
85
+ * the node becomes a pure group whose children include `_self` plus the
86
+ * original children (each recursively processed).
87
+ *
88
+ * @param {object} node A single DTCG node object.
89
+ * @returns {object} The normalised node.
90
+ */
91
+ function processNode(node) {
92
+ if (!node || typeof node !== 'object') return node;
93
+
94
+ const hasValue = '$value' in node;
95
+ const nonDollarChildren = Object.keys(node).filter((k) => !k.startsWith('$'));
96
+ const hasChildren = nonDollarChildren.length > 0;
97
+
98
+ if (hasValue && hasChildren) {
99
+ // ── Dual-role node ────────────────────────────────────────────────────────
100
+ // Collect the DTCG metadata into a `_self` leaf.
101
+ const selfLeaf = {};
102
+ for (const [k, v] of Object.entries(node)) {
103
+ if (k.startsWith('$')) selfLeaf[k] = v;
104
+ }
105
+ // Build the pure-group: _self first, then recursively-processed children.
106
+ const group = { _self: selfLeaf };
107
+ for (const childKey of nonDollarChildren) {
108
+ group[childKey] = processNode(node[childKey]);
109
+ }
110
+ return group;
111
+ } else if (hasChildren) {
112
+ // ── Pure group ────────────────────────────────────────────────────────────
113
+ const out = {};
114
+ for (const [k, v] of Object.entries(node)) {
115
+ if (k.startsWith('$')) {
116
+ out[k] = v;
117
+ } else {
118
+ out[k] = processNode(v);
119
+ }
120
+ }
121
+ return out;
122
+ } else {
123
+ // ── Pure leaf token ───────────────────────────────────────────────────────
124
+ return node;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Top-level preprocessor function.
130
+ *
131
+ * The top-level dictionary itself is always a group (never a token), so we
132
+ * iterate its entries and call processNode() on each non-`$` child.
133
+ *
134
+ * @param {object} dictionary Raw DTCG token tree from the theme JSON file
135
+ * @returns {object} Normalised tree with all dual-role nodes split
136
+ */
137
+ function carbonDualRolePreprocessor(dictionary) {
138
+ if (!dictionary || typeof dictionary !== 'object') return dictionary;
139
+
140
+ const out = {};
141
+ for (const [key, value] of Object.entries(dictionary)) {
142
+ if (key.startsWith('$')) {
143
+ // Top-level DTCG metadata — preserve as-is.
144
+ out[key] = value;
145
+ } else if (value && typeof value === 'object') {
146
+ out[key] = processNode(value);
147
+ } else {
148
+ out[key] = value;
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+
154
+ module.exports = {
155
+ name: 'carbon/dual-role',
156
+ preprocessor: carbonDualRolePreprocessor,
157
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ *
12
+ * Extracts the `$extensions["org.carbon"]["color-scheme"]` value that lives
13
+ * at the top level of each theme JSON file (white.json, g10.json, etc.) and
14
+ * injects it as a synthetic `color-scheme` token, so the SCSS format can
15
+ * include it in the theme Sass map alongside the regular color tokens.
16
+ *
17
+ *
18
+ * @param {object} dictionary Style Dictionary raw token tree
19
+ * @returns {object}
20
+ */
21
+ function carbonThemeMetadataPreprocessor(dictionary) {
22
+ const colorScheme = dictionary.$extensions?.['org.carbon']?.['color-scheme'];
23
+
24
+ if (!colorScheme) {
25
+ return dictionary;
26
+ }
27
+
28
+ // Inject as color.scheme so the flattened token name becomes 'color-scheme',
29
+ // matching what dtcg-converter.js produces today.
30
+ return {
31
+ ...dictionary,
32
+ color: {
33
+ ...(dictionary.color ?? {}),
34
+ scheme: {
35
+ $type: 'other',
36
+ $value: colorScheme,
37
+ $description: 'Color scheme of this theme (light or dark)',
38
+ },
39
+ },
40
+ };
41
+ }
42
+
43
+ module.exports = {
44
+ name: 'carbon/theme-metadata',
45
+ preprocessor: carbonThemeMetadataPreprocessor,
46
+ };
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ * Style Dictionary configuration for @carbon/themes.
12
+ *
13
+ * Wires together the custom plugins in this directory
14
+ *
15
+ */
16
+
17
+ const path = require('path');
18
+ const fs = require('fs-extra');
19
+ const { default: StyleDictionary } = require('style-dictionary');
20
+
21
+ // ── Custom plugins ────────────────────────────────────────────────────────────
22
+ const carbonColorFlatten = require('./transforms/color-flatten');
23
+ const carbonAlphaModifier = require('./transforms/alpha-modifier');
24
+ const carbonComponentTokensPreprocessor = require('./preprocessors/component-tokens');
25
+ const carbonThemeMetadataPreprocessor = require('./preprocessors/theme-metadata');
26
+ const { preprocessor: flattenDualRole } = require('./preprocessors/dual-role');
27
+ const carbonScssThemes = require('./formats/scss-themes');
28
+ const carbonScssTokens = require('./formats/scss-tokens');
29
+ const carbonScssComponentTokens = require('./formats/scss-component-tokens');
30
+ const carbonJsThemes = require('./formats/js-themes');
31
+ const carbonJsComponentTokens = require('./formats/js-component-tokens');
32
+
33
+ // ── Paths ─────────────────────────────────────────────────────────────────────
34
+ const ROOT = path.resolve(__dirname, '..');
35
+ const DTCG_DIR = path.join(ROOT, 'src', 'dtcg');
36
+ const SCSS_GENERATED = path.join(ROOT, 'scss', 'generated');
37
+ const JS_GENERATED_THEMES = path.join(ROOT, 'js', 'generated', 'themes');
38
+ const JS_GENERATED_COMPONENTS = path.join(
39
+ ROOT,
40
+ 'js',
41
+ 'generated',
42
+ 'component-tokens'
43
+ );
44
+
45
+ const THEME_NAMES = ['white', 'g10', 'g90', 'g100'];
46
+ const COMPONENT_NAMES = [
47
+ 'button',
48
+ 'tag',
49
+ 'notification',
50
+ 'status',
51
+ 'content-switcher',
52
+ ];
53
+
54
+ // ── Custom name transform ─────────────────────────────────────────────────────
55
+ //
56
+ // Builds a kebab-case name from the SD token path, with one special rule:
57
+ // a path segment equal to `_self` is stripped. This is used in conjunction
58
+ // with the carbon/dual-role preprocessor — dual-role parent token values are
59
+ // placed under a `_self` child key so that SD processes them as separate leaf
60
+ // tokens, but their output name omits the `_self` suffix.
61
+ //
62
+ // Examples:
63
+ // ['background'] → 'background'
64
+ // ['background', '_self'] → 'background' (parent of dual-role node)
65
+ // ['background', 'active'] → 'background-active'
66
+ // ['layer', '01'] → 'layer-01'
67
+ // ['layer', '_self'] → 'layer'
68
+ const carbonNameKebab = {
69
+ name: 'carbon/name-kebab',
70
+ type: 'name',
71
+ transform(token) {
72
+ return token.path
73
+ .filter((p) => p !== '_self')
74
+ .map((p) => p.replace(/^_/, '').replace(/_/g, '-'))
75
+ .join('-');
76
+ },
77
+ };
78
+
79
+ // ── Transform group ────────────────────────────────────────────────────────────
80
+ //
81
+ // Order matters: alpha-modifier must fire BEFORE color-flatten so the hex
82
+ // is already resolved when flatten runs.
83
+ // Both are `transitive: true` — SD re-applies them until the value stabilises.
84
+ const CARBON_TRANSFORMS = [
85
+ 'attribute/cti',
86
+ 'carbon/name-kebab', // custom: strips _self from dual-role parent paths
87
+ 'carbon/alpha-modifier',
88
+ 'carbon/color-flatten',
89
+ ];
90
+
91
+ // ── Helper: build config for one theme ───────────────────────────────────────
92
+ function themeConfig(themeName) {
93
+ // Pre-process the theme JSON to handle "dual-role" nodes (nodes that have
94
+ // both a $value AND non-$ children). SD v5 treats a node with $value as a
95
+ // leaf and silently drops children; flattenDualRole collects ALL leaf tokens
96
+ // (including children of dual-role parents) into a flat camelCase structure.
97
+ // The palette is kept in `source:` (nested) so SD alias resolution works.
98
+ const rawTheme = JSON.parse(
99
+ fs.readFileSync(path.join(DTCG_DIR, `${themeName}.json`), 'utf8')
100
+ );
101
+ const flatTokens = flattenDualRole(rawTheme);
102
+
103
+ return {
104
+ source: [path.join(DTCG_DIR, 'color-palette.json')],
105
+ tokens: flatTokens,
106
+ preprocessors: ['carbon/theme-metadata'],
107
+ platforms: {
108
+ // SCSS theme map
109
+ [`scss/themes/${themeName}`]: {
110
+ transformGroup: 'carbon',
111
+ buildPath: SCSS_GENERATED + '/',
112
+ files: [
113
+ {
114
+ // Each theme writes to its own temp file; a post-step
115
+ // (buildAllPlatforms action) concatenates them into _themes.scss.
116
+ destination: `_theme-${themeName}.scss`,
117
+ format: 'carbon/scss-themes',
118
+ options: { themeName },
119
+ // Exclude palette reference tokens from the theme map.
120
+ filter(token) {
121
+ const role =
122
+ token.$extensions?.['org.carbon']?.role ??
123
+ token.extensions?.['org.carbon']?.role;
124
+ return role !== 'reference';
125
+ },
126
+ },
127
+ ],
128
+ },
129
+ // JS theme module
130
+ [`js/themes/${themeName}`]: {
131
+ transformGroup: 'carbon',
132
+ buildPath: JS_GENERATED_THEMES + '/',
133
+ files: [
134
+ {
135
+ destination: `${themeName}.js`,
136
+ format: 'carbon/js-themes',
137
+ options: { themeName, output: 'js' },
138
+ filter(token) {
139
+ const role =
140
+ token.$extensions?.['org.carbon']?.role ??
141
+ token.extensions?.['org.carbon']?.role;
142
+ return role !== 'reference';
143
+ },
144
+ },
145
+ {
146
+ destination: `${themeName}.d.ts`,
147
+ format: 'carbon/js-themes',
148
+ options: { themeName, output: 'dts' },
149
+ filter(token) {
150
+ const role =
151
+ token.$extensions?.['org.carbon']?.role ??
152
+ token.extensions?.['org.carbon']?.role;
153
+ return role !== 'reference';
154
+ },
155
+ },
156
+ ],
157
+ },
158
+ },
159
+ };
160
+ }
161
+
162
+ // ── Helper: build config for one component ───────────────────────────────────
163
+ function componentConfig(componentName) {
164
+ return {
165
+ source: [
166
+ path.join(DTCG_DIR, 'color-palette.json'),
167
+ path.join(DTCG_DIR, 'components', `${componentName}.json`),
168
+ ],
169
+ preprocessors: ['carbon/component-tokens'],
170
+ platforms: {
171
+ // SCSS component token map
172
+ [`scss/component-tokens/${componentName}`]: {
173
+ transformGroup: 'carbon',
174
+ buildPath: SCSS_GENERATED + '/',
175
+ files: [
176
+ {
177
+ destination: `_${componentName}-tokens.scss`,
178
+ format: 'carbon/scss-component-tokens',
179
+ filter(token) {
180
+ // Only include expanded theme tokens (injected by preprocessor)
181
+ return token.name.includes('-by-theme-');
182
+ },
183
+ },
184
+ ],
185
+ },
186
+ // JS component token module
187
+ [`js/component-tokens/${componentName}`]: {
188
+ transformGroup: 'carbon',
189
+ buildPath: JS_GENERATED_COMPONENTS + '/',
190
+ files: [
191
+ {
192
+ destination: `${componentName}.js`,
193
+ format: 'carbon/js-component-tokens',
194
+ options: { output: 'js' },
195
+ filter(token) {
196
+ return token.name.includes('-by-theme-');
197
+ },
198
+ },
199
+ {
200
+ destination: `${componentName}.d.ts`,
201
+ format: 'carbon/js-component-tokens',
202
+ options: { output: 'dts' },
203
+ filter(token) {
204
+ return token.name.includes('-by-theme-');
205
+ },
206
+ },
207
+ ],
208
+ },
209
+ },
210
+ };
211
+ }
212
+
213
+ // ── _tokens.scss config ───────────────────────────────────────────────────────
214
+ //
215
+ // This file does not use token values at all — it reads from Carbon JS metadata
216
+ // directly. We still run it through SD so it participates in buildAllPlatforms
217
+ // and can be tested alongside the other outputs.
218
+ const tokensConfig = {
219
+ // No source files needed — format reads from src/tokens directly.
220
+ source: [],
221
+ platforms: {
222
+ 'scss/tokens': {
223
+ transformGroup: 'carbon',
224
+ buildPath: SCSS_GENERATED + '/',
225
+ files: [
226
+ {
227
+ destination: '_tokens.scss',
228
+ format: 'carbon/scss-tokens',
229
+ },
230
+ ],
231
+ },
232
+ },
233
+ };
234
+
235
+ // ── Build a registered SD instance ────────────────────────────────────────────
236
+ // In SD v5, register* methods live on the instance, not the class.
237
+ // We create one base instance with all plugins registered, then extend it
238
+ // per-config so each build inherits the registrations.
239
+ function createBase() {
240
+ const base = new StyleDictionary({});
241
+ // Register custom transforms BEFORE the transform group that references them.
242
+ base.registerTransform(carbonNameKebab);
243
+ base.registerTransform(carbonAlphaModifier);
244
+ base.registerTransform(carbonColorFlatten);
245
+ base.registerTransformGroup({
246
+ name: 'carbon',
247
+ transforms: CARBON_TRANSFORMS,
248
+ });
249
+ base.registerPreprocessor(carbonComponentTokensPreprocessor);
250
+ base.registerPreprocessor(carbonThemeMetadataPreprocessor);
251
+ base.registerFormat(carbonScssThemes);
252
+ base.registerFormat(carbonScssTokens);
253
+ base.registerFormat(carbonScssComponentTokens);
254
+ base.registerFormat(carbonJsThemes);
255
+ base.registerFormat(carbonJsComponentTokens);
256
+ return base;
257
+ }
258
+
259
+ // ── SCSS build ────────────────────────────────────────────────────────────────
260
+ // Generates:
261
+ // scss/generated/_themes.scss
262
+ // scss/generated/_tokens.scss
263
+ // scss/generated/_button-tokens.scss (and tag, notification, status, content-switcher)
264
+ async function runScss() {
265
+ const base = createBase();
266
+ const scssTokensContent = await carbonScssTokens.format({
267
+ dictionary: { allTokens: [] },
268
+ options: {},
269
+ file: {},
270
+ });
271
+ fs.ensureDirSync(SCSS_GENERATED);
272
+ fs.writeFileSync(
273
+ path.join(SCSS_GENERATED, '_tokens.scss'),
274
+ scssTokensContent,
275
+ 'utf8'
276
+ );
277
+
278
+ // Per-theme maps — write to temp files, then concatenate
279
+ for (const themeName of THEME_NAMES) {
280
+ await (await base.extend(themeConfig(themeName))).buildAllPlatforms();
281
+ }
282
+
283
+ // Concatenate per-theme temp files into a single _themes.scss
284
+ const themeParts = THEME_NAMES.map((name) => {
285
+ const src = path.join(SCSS_GENERATED, `_theme-${name}.scss`);
286
+ const content = fs.readFileSync(src, 'utf8');
287
+ return name === THEME_NAMES[0]
288
+ ? content
289
+ : '\n' +
290
+ content
291
+ .replace(/^\/\/[^\n]*\n(?:\/\/[^\n]*\n)*/m, '') // strip banner
292
+ .replace(/^@use [^\n]+\n/gm, '') // strip duplicate @use
293
+ .replace(/^\n+/, ''); // collapse leading blanks
294
+ });
295
+ fs.writeFileSync(
296
+ path.join(SCSS_GENERATED, '_themes.scss'),
297
+ themeParts.join(''),
298
+ 'utf8'
299
+ );
300
+ for (const name of THEME_NAMES) {
301
+ fs.removeSync(path.join(SCSS_GENERATED, `_theme-${name}.scss`));
302
+ }
303
+
304
+ // Component token maps
305
+ for (const componentName of COMPONENT_NAMES) {
306
+ await (
307
+ await base.extend(componentConfig(componentName))
308
+ ).buildAllPlatforms();
309
+ }
310
+ }
311
+
312
+ // ── JS build ──────────────────────────────────────────────────────────────────
313
+ // Generates:
314
+ // js/generated/themes/{white,g10,g90,g100}.{js,d.ts}
315
+ // js/generated/component-tokens/{button,tag,…}.{js,d.ts}
316
+ async function runJs() {
317
+ const base = createBase();
318
+
319
+ for (const themeName of THEME_NAMES) {
320
+ await (await base.extend(themeConfig(themeName))).buildAllPlatforms();
321
+ }
322
+ for (const componentName of COMPONENT_NAMES) {
323
+ await (
324
+ await base.extend(componentConfig(componentName))
325
+ ).buildAllPlatforms();
326
+ }
327
+ }
328
+
329
+ // ── Full build (SCSS + JS) ────────────────────────────────────────────────────
330
+ async function run() {
331
+ await runScss();
332
+ await runJs();
333
+ }
334
+
335
+ module.exports = {
336
+ run,
337
+ runScss,
338
+ runJs,
339
+ themeConfig,
340
+ componentConfig,
341
+ tokensConfig,
342
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ *
12
+ * Applies the `$extensions["org.carbon"].alphaModifier` field to theme tokens,
13
+ * producing an `rgba()` string.
14
+ *
15
+ * Must run AFTER alias resolution (transitive: true) so that by the time this
16
+ * transform fires, `token.value` is already the resolved hex string from the
17
+ * palette (e.g. '#8d8d8d' for gray.50), not the raw alias '{gray.50}'.
18
+ *
19
+ * If no alphaModifier extension is present the token is passed through
20
+ * unchanged (carbon/color-flatten will handle it next).
21
+ *
22
+ * Example in white.json:
23
+ * "background-active": {
24
+ * "$type": "color",
25
+ * "$value": "{gray.50}", ← resolved by SD to '#8d8d8d'
26
+ * "$extensions": {
27
+ * "org.carbon": { "alphaModifier": 0.5 }
28
+ * }
29
+ * }
30
+ * → 'rgba(141, 141, 141, 0.5)'
31
+ */
32
+
33
+ module.exports = {
34
+ name: 'carbon/alpha-modifier',
35
+ type: 'value',
36
+ transitive: true,
37
+ filter(token) {
38
+ const ext =
39
+ token.$extensions?.['org.carbon'] ?? token.extensions?.['org.carbon'];
40
+ return ext?.alphaModifier !== undefined;
41
+ },
42
+ transform(token) {
43
+ const ext =
44
+ token.$extensions?.['org.carbon'] ?? token.extensions?.['org.carbon'];
45
+ const alpha = ext.alphaModifier;
46
+
47
+ // SD v5: post-alias value is in token.$value (resolved object or hex string).
48
+ // In the golden-master test, token.value holds the pre-resolved hex string.
49
+ const resolved = token.value !== undefined ? token.value : token.$value;
50
+ const hex =
51
+ typeof resolved === 'string'
52
+ ? resolved
53
+ : typeof resolved === 'object' && resolved?.hex
54
+ ? resolved.hex
55
+ : null;
56
+
57
+ if (!hex || !hex.startsWith('#') || hex.length < 7) {
58
+ // Fallback: if resolution hasn't happened yet, return unchanged and let
59
+ // the transitive pass retry.
60
+ return token.value ?? token.$value;
61
+ }
62
+
63
+ const r = parseInt(hex.slice(1, 3), 16);
64
+ const g = parseInt(hex.slice(3, 5), 16);
65
+ const b = parseInt(hex.slice(5, 7), 16);
66
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
67
+ },
68
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Copyright IBM Corp. 2026
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ *
12
+ * Converts the DTCG color $value object shapes used by this codebase into
13
+ * plain CSS color strings, after Style Dictionary has already resolved any
14
+ * alias references.
15
+ *
16
+ * Input shapes (post-alias-resolution, i.e. $value is already an object):
17
+ *
18
+ * Shape A — solid inline object
19
+ * { colorSpace: 'srgb', components: [...], hex: '#rrggbb' }
20
+ * → '#rrggbb'
21
+ *
22
+ * Shape B — alpha inline object (components + alpha, no hex)
23
+ * { colorSpace: 'srgb', components: [r, g, b], alpha: 0.5 }
24
+ * → 'rgba(r, g, b, 0.5)'
25
+ *
26
+ * Plain strings and non-sRGB objects are passed through unchanged.
27
+ *
28
+ * NOTE: The alias+alphaModifier case (Shape C) is handled upstream by the
29
+ * carbon/alpha-modifier transform, which runs first (transitive: true ensures
30
+ * the resolved alias value is already in scope when this transform fires).
31
+ */
32
+
33
+ module.exports = {
34
+ name: 'carbon/color-flatten',
35
+ type: 'value',
36
+ transitive: true,
37
+ filter(token) {
38
+ return token.$type === 'color' || token.type === 'color';
39
+ },
40
+ transform(token) {
41
+ // SD v5 stores the post-alias-resolution value in token.$value (the DTCG
42
+ // field), NOT in token.value. token.value is only set after transforms run.
43
+ // The golden-master test sets both; in that context token.value is preferred
44
+ // so the test can simulate resolution without running full SD.
45
+ const v = token.value !== undefined ? token.value : token.$value;
46
+
47
+ if (typeof v === 'string') {
48
+ // Already a plain string (e.g. '#0f62fe' or 'rgba(...)') — pass through.
49
+ return v;
50
+ }
51
+
52
+ if (v === null || typeof v !== 'object') {
53
+ return v;
54
+ }
55
+
56
+ // SD v5 can resolve an alias to the full token node instead of its value
57
+ // when the alias points to a sibling token (e.g. {syntax.value} resolving
58
+ // to the syntax.value token object). Unwrap one level.
59
+ if ('$value' in v) {
60
+ const inner = v.$value;
61
+ if (typeof inner === 'string') return inner;
62
+ if (inner?.hex) return inner.hex;
63
+ }
64
+
65
+ if (v.colorSpace !== 'srgb' || !Array.isArray(v.components)) {
66
+ return v;
67
+ }
68
+
69
+ // Shape A — solid: hex present
70
+ if (typeof v.hex === 'string') {
71
+ return v.hex;
72
+ }
73
+
74
+ // Shape B — alpha: derive rgba from 0–1 component floats
75
+ const [r, g, b] = v.components.map((c) => Math.round(c * 255));
76
+ const alpha = v.alpha !== undefined ? v.alpha : 1;
77
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
78
+ },
79
+ };