@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.
- package/es/index.js +7 -7
- package/js/generated/themes/g10.d.ts +2 -2
- package/js/generated/themes/g10.js +2 -2
- package/js/generated/themes/g100.d.ts +2 -2
- package/js/generated/themes/g100.js +2 -2
- package/js/generated/themes/g90.d.ts +2 -2
- package/js/generated/themes/g90.js +2 -2
- package/js/generated/themes/white.d.ts +1 -1
- package/js/generated/themes/white.js +1 -1
- package/lib/index.js +7 -7
- package/package.json +11 -7
- package/src/dtcg/color-palette.json +1464 -244
- package/style-dictionary/formats/js-component-tokens.js +128 -0
- package/style-dictionary/formats/js-themes.js +84 -0
- package/style-dictionary/formats/scss-component-tokens.js +149 -0
- package/style-dictionary/formats/scss-themes.js +127 -0
- package/style-dictionary/formats/scss-tokens.js +91 -0
- package/style-dictionary/preprocessors/component-tokens.js +119 -0
- package/style-dictionary/preprocessors/dual-role.js +157 -0
- package/style-dictionary/preprocessors/theme-metadata.js +46 -0
- package/style-dictionary/sd.config.js +342 -0
- package/style-dictionary/transforms/alpha-modifier.js +68 -0
- package/style-dictionary/transforms/color-flatten.js +79 -0
- package/umd/index.js +7 -7
|
@@ -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
|
+
};
|