@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,128 @@
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
+ * Receives tokens for ONE component (pre-processed by
12
+ * `component-tokens`) and emits two files:
13
+ * - `<componentName>.js` — named ES exports, one per token
14
+ * - `<componentName>.d.ts` — companion TypeScript declarations
15
+ *
16
+ */
17
+
18
+ const FILE_BANNER = `// Code generated by @carbon/themes. DO NOT EDIT.
19
+ //
20
+ // Copyright IBM Corp. 2018, 2025
21
+ //
22
+ // This source code is licensed under the Apache-2.0 license found in the
23
+ // LICENSE file in the root directory of this source tree.
24
+ `;
25
+
26
+ /**
27
+ * Convert kebab-case to camelCase.
28
+ * @param {string} str
29
+ * @returns {string}
30
+ */
31
+ function kebabToCamel(str) {
32
+ return str.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase());
33
+ }
34
+
35
+ /**
36
+ * Normalize theme key to the JS API convention.
37
+ * 'white' → 'whiteTheme', 'g10' → 'g10' (no dash, unlike SCSS convention)
38
+ * @param {string} theme
39
+ * @returns {string}
40
+ */
41
+ function normalizeThemeKey(theme) {
42
+ if (theme === 'white') return 'whiteTheme';
43
+ return theme; // 'g10', 'g90', 'g100' — unchanged
44
+ }
45
+
46
+ /**
47
+ * Serialize a JS value to source-code string.
48
+ * @param {*} value
49
+ * @returns {string}
50
+ */
51
+ function toSource(value) {
52
+ if (typeof value === 'string') {
53
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
54
+ }
55
+ return JSON.stringify(value);
56
+ }
57
+
58
+ /**
59
+ * Group SD tokens by component-token name and theme.
60
+ * Mirrors the logic in carbon-scss-component-tokens.js.
61
+ *
62
+ * @param {import('style-dictionary').Token[]} allTokens
63
+ * @returns {Map<string, Record<string, string>>}
64
+ */
65
+ function groupByTokenAndTheme(allTokens) {
66
+ const result = new Map();
67
+ const BY_THEME_MARKER = '-by-theme-';
68
+
69
+ for (const token of allTokens) {
70
+ const idx = token.name.indexOf(BY_THEME_MARKER);
71
+ if (idx === -1) continue;
72
+
73
+ const tokenName = token.name.slice(0, idx);
74
+ const theme = token.name.slice(idx + BY_THEME_MARKER.length);
75
+
76
+ if (!result.has(tokenName)) {
77
+ result.set(tokenName, {});
78
+ }
79
+ result.get(tokenName)[theme] = token.value ?? token.$value;
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * @param {{ dictionary: import('style-dictionary').Dictionary, options: object }} args
87
+ * @returns {string}
88
+ */
89
+ function carbonJsComponentTokensFormat({ dictionary, options }) {
90
+ const outputMode = options?.output ?? 'js'; // 'js' | 'dts'
91
+
92
+ const grouped = groupByTokenAndTheme(dictionary.allTokens);
93
+
94
+ // Preserve insertion order from the DTCG JSON walk.
95
+ const sortedNames = [...grouped.keys()];
96
+
97
+ const lines = [FILE_BANNER];
98
+
99
+ for (const tokenName of sortedNames) {
100
+ const themeValues = grouped.get(tokenName);
101
+ const camelName = kebabToCamel(tokenName);
102
+
103
+ // Preserve JSON source order exactly.
104
+ const themes = Object.keys(themeValues);
105
+
106
+ if (outputMode === 'js') {
107
+ const props = themes
108
+ .map((theme) => {
109
+ const key = normalizeThemeKey(theme);
110
+ return ` ${key}: ${toSource(themeValues[theme])}`;
111
+ })
112
+ .join(',\n');
113
+ lines.push(`export const ${camelName} = {\n${props},\n};`);
114
+ } else {
115
+ const props = themes
116
+ .map((theme) => ` ${normalizeThemeKey(theme)}: string`)
117
+ .join(';\n');
118
+ lines.push(`export declare const ${camelName}: {\n${props};\n};`);
119
+ }
120
+ }
121
+
122
+ return lines.join('\n') + '\n';
123
+ }
124
+
125
+ module.exports = {
126
+ name: 'carbon/js-component-tokens',
127
+ format: carbonJsComponentTokensFormat,
128
+ };
@@ -0,0 +1,84 @@
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
+ * Receives all tokens for ONE theme and emits two files (via SD's
13
+ * `files` array):
14
+ * - `<themeName>.js` — ES module with `export const camelName = value;`
15
+ * - `<themeName>.d.ts` — companion TypeScript declarations
16
+ *
17
+ * Token names arrive as kebab-case from SD and are converted to camelCase.
18
+ *
19
+ */
20
+
21
+ const FILE_BANNER = `// Code generated by @carbon/themes. DO NOT EDIT.
22
+ //
23
+ // Copyright IBM Corp. 2018, 2025
24
+ //
25
+ // This source code is licensed under the Apache-2.0 license found in the
26
+ // LICENSE file in the root directory of this source tree.
27
+ `;
28
+
29
+ /**
30
+ * Convert kebab-case to camelCase.
31
+ * @param {string} str
32
+ * @returns {string}
33
+ */
34
+ function kebabToCamel(str) {
35
+ return str.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase());
36
+ }
37
+
38
+ /**
39
+ * Serialize a JS value to a source-code string.
40
+ * Strings get single-quoted; everything else uses JSON.
41
+ * @param {*} value
42
+ * @returns {string}
43
+ */
44
+ function toSource(value) {
45
+ if (typeof value === 'string') {
46
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
47
+ }
48
+ return JSON.stringify(value);
49
+ }
50
+
51
+ /**
52
+ * @param {{ dictionary: import('style-dictionary').Dictionary, options: object }} args
53
+ * @returns {string}
54
+ */
55
+ function carbonJsThemesFormat({ dictionary, options }) {
56
+ const outputMode = options?.output ?? 'js'; // 'js' | 'dts'
57
+
58
+ const lines = [FILE_BANNER];
59
+ const sorted = dictionary.allTokens;
60
+
61
+ for (const token of sorted) {
62
+ const camelName = kebabToCamel(token.name);
63
+ // SD v5 may store the resolved alias as a full token node object in
64
+ // token.value when the alias points to a sibling token. Unwrap it.
65
+ let value = token.value ?? token.$value;
66
+ if (value !== null && typeof value === 'object' && '$value' in value) {
67
+ value = value.$value;
68
+ }
69
+ const tsType = typeof value === 'string' ? 'string' : 'number';
70
+
71
+ if (outputMode === 'js') {
72
+ lines.push(`export const ${camelName} = ${toSource(value)};`);
73
+ } else {
74
+ lines.push(`export declare const ${camelName}: ${tsType};`);
75
+ }
76
+ }
77
+
78
+ return lines.join('\n') + '\n';
79
+ }
80
+
81
+ module.exports = {
82
+ name: 'carbon/js-themes',
83
+ format: carbonJsThemesFormat,
84
+ };
@@ -0,0 +1,149 @@
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
+ * Receives the tokens for ONE component (already expanded from the
12
+ * carbon.themes multi-theme shape by the `component-tokens`
13
+ * preprocessor). Emits a Sass map variable per token, keyed by normalised
14
+ * theme name:
15
+ *
16
+ * $button-danger-active: (
17
+ * 'white-theme': #a2191f,
18
+ * 'g-10': #a2191f,
19
+ * 'g-90': #a2191f,
20
+ * 'g-100': #a2191f,
21
+ * ) !default;
22
+ *
23
+ *
24
+ * The token tree must have been pre-processed by `carbon/component-tokens`
25
+ * so that each token has a `_by_theme` child group containing one resolved
26
+ * token per theme.
27
+ */
28
+
29
+ const FILE_BANNER = `// Code generated by @carbon/themes. DO NOT EDIT.
30
+ //
31
+ // Copyright IBM Corp. 2018, 2023
32
+ //
33
+ // This source code is licensed under the Apache-2.0 license found in the
34
+ // LICENSE file in the root directory of this source tree.
35
+ //
36
+ `;
37
+
38
+ /**
39
+ * Normalize a raw theme key to the Carbon SCSS convention.
40
+ * @param {string} theme
41
+ * @returns {string}
42
+ */
43
+ function normalizeThemeName(theme) {
44
+ if (theme === 'white') return 'white-theme';
45
+ if (/^g\d+$/.test(theme)) return theme.replace(/^g(\d+)$/, 'g-$1');
46
+ return theme;
47
+ }
48
+
49
+ /**
50
+ * Serialize a CSS value to a Sass literal (same logic as carbon-scss-themes).
51
+ * @param {string|number} value
52
+ * @returns {string}
53
+ */
54
+ function toSassLiteral(value) {
55
+ if (typeof value === 'number') return String(value);
56
+ if (typeof value !== 'string') return JSON.stringify(value);
57
+ if (value.startsWith('#') || value.startsWith('rgb')) return value;
58
+ if (
59
+ value.endsWith('px') ||
60
+ value.endsWith('em') ||
61
+ value.endsWith('%') ||
62
+ value === '0'
63
+ )
64
+ return value;
65
+ return `string.unquote("${value}")`;
66
+ }
67
+
68
+ /**
69
+ * Group SD tokens by their component-token name and theme.
70
+ *
71
+ * After the carbon/component-tokens preprocessor runs, tokens in the
72
+ * dictionary look like:
73
+ *
74
+ * button-danger-active-by-theme-white (name after flattening)
75
+ * button-danger-active-by-theme-g10
76
+ * ...
77
+ *
78
+ * We strip the `_by_theme-` infix and reconstruct the map.
79
+ *
80
+ * @param {import('style-dictionary').Token[]} allTokens
81
+ * @returns {Map<string, Record<string, string>>} tokenName → { theme: value }
82
+ */
83
+ function groupByTokenAndTheme(allTokens) {
84
+ const result = new Map();
85
+
86
+ for (const token of allTokens) {
87
+ // Token names contain '_by_theme' as an injected path segment.
88
+ // e.g. 'button-danger-active-by-theme-white' — but SD uses whatever
89
+ // separator the platform config specifies (usually '-').
90
+ // Match the segment just before a known theme key.
91
+ const BY_THEME_MARKER = '-by-theme-';
92
+ const idx = token.name.indexOf(BY_THEME_MARKER);
93
+ if (idx === -1) continue;
94
+
95
+ const tokenName = token.name.slice(0, idx);
96
+ const theme = token.name.slice(idx + BY_THEME_MARKER.length);
97
+
98
+ if (!result.has(tokenName)) {
99
+ result.set(tokenName, {});
100
+ }
101
+ result.get(tokenName)[theme] = token.value ?? token.$value;
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * @param {{ dictionary: import('style-dictionary').Dictionary }} args
109
+ * @returns {string}
110
+ */
111
+ function carbonScssComponentTokensFormat({ dictionary }) {
112
+ const grouped = groupByTokenAndTheme(dictionary.allTokens);
113
+
114
+ // Preserve insertion order from the DTCG JSON walk — do NOT sort here.
115
+ // The old builder also sorted alphabetically, but the compare script passes
116
+ // tokens in JSON order so we honour that to get a byte-for-byte match.
117
+ const sortedNames = [...grouped.keys()];
118
+
119
+ const variableBlocks = sortedNames.map((tokenName) => {
120
+ const themeValues = grouped.get(tokenName);
121
+
122
+ // Preserve JSON source order exactly.
123
+ const themes = Object.keys(themeValues);
124
+
125
+ const entries = themes
126
+ .map((theme) => {
127
+ const normalized = normalizeThemeName(theme);
128
+ const value = themeValues[theme];
129
+ // Unquoted key — matches t.SassMapProperty(t.Identifier(theme), ...) in old builder
130
+ return ` ${normalized}: ${toSassLiteral(value)},`;
131
+ })
132
+ .join('\n');
133
+
134
+ return `\n$${tokenName}: (\n${entries}\n) !default;`;
135
+ });
136
+
137
+ return (
138
+ FILE_BANNER +
139
+ `\n@use 'sass:map';\n` +
140
+ `@use 'sass:string';\n` +
141
+ variableBlocks.join('\n') +
142
+ '\n'
143
+ );
144
+ }
145
+
146
+ module.exports = {
147
+ name: 'carbon/scss-component-tokens',
148
+ format: carbonScssComponentTokensFormat,
149
+ };
@@ -0,0 +1,127 @@
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
+ *
13
+ * Each theme (white, g10, g90, g100) is expected to be a separate SD
14
+ * *platform* build run. This format receives all tokens for ONE theme and
15
+ * emits the Sass map variable for that theme plus the utilities.merge call
16
+ * that folds in layout spacing and type tokens.
17
+ *
18
+ * ─────────────────────────────────────────────────────────────────────────────
19
+ * NOTE ON MULTI-THEME FILES
20
+ * ─────────────────────────────────────────────────────────────────────────────
21
+ * The recommended approach: run all four platforms, collect their individual
22
+ * strings, and concatenate them in sd.config.js using a custom action.
23
+ * A simpler alternative (and the one wired up in sd.config.js) is to use SD's
24
+ * `buildAllPlatforms()` with four platforms that each write to a *separate*
25
+ * file, then a post-build step (`buildAllPlatforms` custom action) concatenates
26
+ * them. Both patterns are documented in style-dictionary/README.md.
27
+ */
28
+
29
+ const FILE_BANNER = `// Code generated by @carbon/themes. DO NOT EDIT.
30
+ //
31
+ // Copyright IBM Corp. 2018, 2023
32
+ //
33
+ // This source code is licensed under the Apache-2.0 license found in the
34
+ // LICENSE file in the root directory of this source tree.
35
+ //
36
+ `;
37
+
38
+ const SCSS_IMPORTS = `@use 'sass:map';
39
+ @use 'sass:string';
40
+ @use '@carbon/layout';
41
+ @use '@carbon/type';
42
+ @use '../utilities';
43
+ `;
44
+
45
+ /**
46
+ * Serialize a CSS color value to a Sass literal.
47
+ *
48
+ * - Hex colors → bare hex (e.g. #ffffff)
49
+ * - rgba() → bare function (e.g. rgba(141, 141, 141, 0.5))
50
+ * - Strings → string.unquote("…")
51
+ * - Numbers → bare number
52
+ *
53
+ * @param {string|number} value
54
+ * @returns {string} Sass source fragment
55
+ */
56
+ function toSassLiteral(value) {
57
+ if (typeof value === 'number') {
58
+ return String(value);
59
+ }
60
+ if (typeof value !== 'string') {
61
+ return JSON.stringify(value);
62
+ }
63
+ if (value.startsWith('#')) {
64
+ return value;
65
+ }
66
+ if (
67
+ value.startsWith('rgb') ||
68
+ value.endsWith('px') ||
69
+ value.endsWith('em') ||
70
+ value.endsWith('%') ||
71
+ value.endsWith('vw') ||
72
+ value.endsWith('vh') ||
73
+ value === '0'
74
+ ) {
75
+ return value;
76
+ }
77
+ // Plain string (e.g. 'light', 'dark') → Sass unquote with single quotes,
78
+ // matching t.SassValue("string.unquote('...')") in the old builder.
79
+ return `string.unquote('${value}')`;
80
+ }
81
+
82
+ /**
83
+ * @param {{ dictionary: import('style-dictionary').Dictionary, options: object, file: object }} args
84
+ * @returns {string}
85
+ */
86
+ function carbonScssThemesFormat({ dictionary, options, file }) {
87
+ const themeName = options?.themeName ?? file?.options?.themeName ?? 'unknown';
88
+
89
+ // Sort alphabetically — matches dtcg-themes.js which calls .sort(([a],[b])=>a.localeCompare(b))
90
+ // color-scheme lands at its natural alpha position ('c' section).
91
+ const sorted = [...dictionary.allTokens].sort((a, b) =>
92
+ a.name.localeCompare(b.name)
93
+ );
94
+
95
+ const entries = sorted
96
+ .map((token) => {
97
+ const name = token.name;
98
+ // SD v5 may store the post-alias value as a full token node object in
99
+ // token.value when an alias points to a sibling token. Unwrap it.
100
+ let value = token.value ?? token.$value;
101
+ if (value !== null && typeof value === 'object' && '$value' in value) {
102
+ value = value.$value;
103
+ }
104
+ // Unquoted key — matches t.SassMapProperty(t.Identifier(token), ...) in old builder
105
+ return ` ${name}: ${toSassLiteral(value)},`;
106
+ })
107
+ .join('\n');
108
+
109
+ return (
110
+ FILE_BANNER +
111
+ '\n' +
112
+ SCSS_IMPORTS +
113
+ '\n' +
114
+ `$${themeName}: (\n${entries}\n) !default;\n` +
115
+ `$${themeName}: utilities.merge(\n` +
116
+ ` $${themeName},\n` +
117
+ ` layout.$spacing,\n` +
118
+ ` layout.$fluid-spacing,\n` +
119
+ ` type.$tokens\n` +
120
+ `);\n`
121
+ );
122
+ }
123
+
124
+ module.exports = {
125
+ name: 'carbon/scss-themes',
126
+ format: carbonScssThemesFormat,
127
+ };
@@ -0,0 +1,91 @@
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
+ * The format does not actually use the SD `dictionary` — it reads the token
12
+ * list from the Carbon JS metadata directly, matching the original builder.
13
+ * This is intentional: _tokens.scss is a mapping layer, not derived from the
14
+ * token values.
15
+ */
16
+
17
+ const { types: t } = require('@carbon/scss-generator');
18
+ const { generate } = require('@carbon/scss-generator');
19
+ const { group } = require('../../src/tokens');
20
+
21
+ const FILE_BANNER = `// Code generated by @carbon/themes. DO NOT EDIT.
22
+ //
23
+ // Copyright IBM Corp. 2018, 2023
24
+ //
25
+ // This source code is licensed under the Apache-2.0 license found in the
26
+ // LICENSE file in the root directory of this source tree.
27
+ //
28
+ `;
29
+
30
+ /**
31
+ * @returns {Promise<string>}
32
+ */
33
+ async function carbonScssTokensFormat() {
34
+ const tokens = group.getTokens();
35
+
36
+ const variables = tokens.flatMap((token) => {
37
+ const id = token.name;
38
+ return [
39
+ t.Newline(),
40
+ t.Comment(`/ The CSS Custom Property for the \`${id}\` token`),
41
+ t.Assignment({
42
+ id: t.Identifier(id),
43
+ init: t.SassFunctionCall({
44
+ id: t.Identifier('_get'),
45
+ params: [t.SassString(id)],
46
+ }),
47
+ default: true,
48
+ }),
49
+ ];
50
+ });
51
+
52
+ const stylesheet = t.StyleSheet([
53
+ t.SassModule('sass:map'),
54
+ t.SassModule('../config'),
55
+ t.SassModule('../theme'),
56
+ t.Newline(),
57
+ t.Comment('/ Internal helper for generating CSS Custom Properties'),
58
+ t.SassFunction({
59
+ id: t.Identifier('_get'),
60
+ params: [t.Identifier('token')],
61
+ body: t.BlockStatement([
62
+ t.IfStatement({
63
+ test: t.LogicalExpression({
64
+ left: t.SassValue('config.$use-fallback-value'),
65
+ operator: '==',
66
+ right: t.SassBoolean(false),
67
+ }),
68
+ consequent: t.BlockStatement([
69
+ t.AtReturn(t.SassValue('var(--#{config.$prefix}-#{$token})')),
70
+ ]),
71
+ alternate: t.BlockStatement([
72
+ t.AtReturn(
73
+ t.SassValue(
74
+ 'var(--#{config.$prefix}-#{$token}, #{theme.get($token)})'
75
+ )
76
+ ),
77
+ ]),
78
+ }),
79
+ ]),
80
+ }),
81
+ ...variables,
82
+ ]);
83
+
84
+ const { code } = generate(stylesheet);
85
+ return FILE_BANNER + '\n' + (await code);
86
+ }
87
+
88
+ module.exports = {
89
+ name: 'carbon/scss-tokens',
90
+ format: carbonScssTokensFormat,
91
+ };
@@ -0,0 +1,119 @@
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
+ * Preprocessor: carbon/component-tokens
12
+ *
13
+ * Expands the `$extensions["carbon.themes"]` multi-theme shape that component
14
+ * token files (button.json, tag.json, …) use into flat per-theme tokens that
15
+ * Style Dictionary can process natively.
16
+ *
17
+ * Input — one token node with no $value but a per-theme map in extensions:
18
+ *
19
+ * "button": {
20
+ * "danger-active": {
21
+ * "$type": "color",
22
+ * "$extensions": {
23
+ * "carbon.themes": { "white": "{red.80}", "g10": "{red.80}", ... },
24
+ * "org.carbon": { "alphaModifiers": { "g90": 0.3, "g100": 0.3 } }
25
+ * }
26
+ * }
27
+ * }
28
+ *
29
+ * Output — one flat sibling token per theme, keyed as
30
+ * `<tokenPath>-_by_theme-<themeName>` so the format functions can reconstruct
31
+ * the original token name and theme:
32
+ *
33
+ * "button": {
34
+ * "danger-active": {
35
+ * "_by_theme": {
36
+ * "white": { "$type": "color", "$value": "{red.80}" },
37
+ * "g10": { "$type": "color", "$value": "{red.80}" },
38
+ * "g90": { "$type": "color", "$value": "{red.80}",
39
+ * "$extensions": { "org.carbon": { "alphaModifier": 0.3 } } },
40
+ * "g100": { "$type": "color", "$value": "{red.80}",
41
+ * "$extensions": { "org.carbon": { "alphaModifier": 0.3 } } }
42
+ * }
43
+ * }
44
+ * }
45
+ *
46
+ * SD v5 walks DTCG token trees and treats any node with a `$value` as a leaf
47
+ * token. The `_by_theme.<themeName>` nodes each have `$value`, so SD resolves
48
+ * aliases and applies transforms on them individually.
49
+ *
50
+ * The format functions identify these tokens by the `-_by_theme-` infix in
51
+ * their flattened name (using the `name/kebab` transform).
52
+ *
53
+ * Tokens that already carry a top-level $value are left untouched.
54
+ *
55
+ * @param {object} dictionary Raw DTCG token tree passed by SD
56
+ * @returns {object} Expanded token tree
57
+ */
58
+ function carbonComponentTokensPreprocessor(dictionary) {
59
+ function walk(node) {
60
+ if (!node || typeof node !== 'object') return node;
61
+
62
+ const out = {};
63
+
64
+ for (const [key, value] of Object.entries(node)) {
65
+ if (key.startsWith('$')) {
66
+ out[key] = value;
67
+ continue;
68
+ }
69
+
70
+ if (!value || typeof value !== 'object') {
71
+ out[key] = value;
72
+ continue;
73
+ }
74
+
75
+ const carbonThemes = value.$extensions?.['carbon.themes'];
76
+ const alphaModifiers =
77
+ value.$extensions?.['org.carbon']?.alphaModifiers ?? {};
78
+
79
+ // Only expand nodes that have carbon.themes and NO existing $value.
80
+ if (carbonThemes && value.$value === undefined) {
81
+ const byTheme = {};
82
+
83
+ // Preserve original JSON key order (important for 'fallback' position)
84
+ for (const [theme, themeValue] of Object.entries(carbonThemes)) {
85
+ const alpha = alphaModifiers[theme];
86
+ const syntheticNode = {
87
+ $type: value.$type ?? 'color',
88
+ $value: themeValue,
89
+ };
90
+ if (alpha !== undefined) {
91
+ syntheticNode.$extensions = {
92
+ 'org.carbon': { alphaModifier: alpha },
93
+ };
94
+ }
95
+ byTheme[theme] = syntheticNode;
96
+ }
97
+
98
+ out[key] = {
99
+ ...(value.$type ? { $type: value.$type } : {}),
100
+ ...(value.$description ? { $description: value.$description } : {}),
101
+ // _by_theme is a DTCG group — each child is a leaf token with $value
102
+ _by_theme: byTheme,
103
+ };
104
+ } else {
105
+ // Recurse into nested groups
106
+ out[key] = walk(value);
107
+ }
108
+ }
109
+
110
+ return out;
111
+ }
112
+
113
+ return walk(dictionary);
114
+ }
115
+
116
+ module.exports = {
117
+ name: 'carbon/component-tokens',
118
+ preprocessor: carbonComponentTokensPreprocessor,
119
+ };