@carbon/motion 11.51.0-rc.0 → 11.52.0-rc.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.
@@ -36,4 +36,4 @@ export declare const slow02: string;
36
36
  // Easing tokens
37
37
  export declare const easings: Record<'standard' | 'entrance' | 'exit', Record<'productive' | 'expressive', string>>;
38
38
 
39
- export declare const unstable_tokens: readonly string[];
39
+ export declare const unstable_tokens: readonly string[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@carbon/motion",
3
3
  "description": "Motion helpers for digital and software products using the Carbon Design System",
4
- "version": "11.51.0-rc.0",
4
+ "version": "11.52.0-rc.0",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": [
7
7
  "index.scss",
@@ -38,13 +38,14 @@
38
38
  "postinstall": "ibmtelemetry --config=telemetry.yml"
39
39
  },
40
40
  "devDependencies": {
41
- "@carbon/cli": "^11.49.0-rc.0",
41
+ "@carbon/cli": "^11.50.0-rc.0",
42
42
  "rimraf": "^6.0.1",
43
+ "style-dictionary": "^5.5.0",
43
44
  "typescript": "^6.0.3",
44
45
  "typescript-config-carbon": "^0.11.0"
45
46
  },
46
47
  "dependencies": {
47
48
  "@ibm/telemetry-js": "^1.5.0"
48
49
  },
49
- "gitHead": "e848b98936051ac0026fb83551c91bfe603c589c"
50
+ "gitHead": "474482a993dff6b9085309aa4bc8484cf71b1612"
50
51
  }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Copyright IBM Corp. 2018, 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
+ * Emits `js/generated/surfaces.{js,d.ts}` from surfaces.json.
12
+ *
13
+ * Surface data lives in `$extensions["carbon.motion"]` on each leaf token —
14
+ * not in `$value` — so Style Dictionary cannot derive the output from the
15
+ * token value alone. This format reads surfaces.json directly (the same
16
+ * pattern used by `carbon/scss-tokens` in @carbon/themes, which reads Carbon
17
+ * JS metadata directly instead of using SD token values).
18
+ *
19
+ * Two files are emitted by wiring this format into two SD `files` entries
20
+ * that differ only in `options.output` ('js' | 'dts').
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const { toDtsTypeLiteral } = require('../utils/dts-type-literal');
26
+
27
+ const FILE_BANNER = `// Code generated by @carbon/motion. DO NOT EDIT.
28
+ //
29
+ // Copyright IBM Corp. 2018, 2026
30
+ //
31
+ // This source code is licensed under the Apache-2.0 license found in the
32
+ // LICENSE file in the root directory of this source tree.
33
+ `;
34
+
35
+ // Path is resolved relative to this file (style-dictionary/formats/) → src/dtcg/
36
+ const SURFACES_PATH = path.resolve(__dirname, '../../src/dtcg/surfaces.json');
37
+
38
+ /**
39
+ * Collect surface entries from surfaces.json.
40
+ * Each surface node carries `$extensions["carbon.motion"]` with the recipe.
41
+ *
42
+ * @returns {Array<{ name: string, description: string, recipe: object }>}
43
+ */
44
+ function collectSurfaces() {
45
+ const dtcg = JSON.parse(fs.readFileSync(SURFACES_PATH, 'utf8'));
46
+ const results = [];
47
+
48
+ function traverse(obj, pathSegments) {
49
+ for (const [key, value] of Object.entries(obj)) {
50
+ if (key.startsWith('$')) continue;
51
+ const segments = [...pathSegments, key];
52
+ if (
53
+ value &&
54
+ typeof value === 'object' &&
55
+ value.$extensions &&
56
+ value.$extensions['carbon.motion']
57
+ ) {
58
+ results.push({
59
+ name: segments.join('-'),
60
+ description: value.$description || '',
61
+ recipe: value.$extensions['carbon.motion'],
62
+ });
63
+ } else if (value && typeof value === 'object') {
64
+ traverse(value, segments);
65
+ }
66
+ }
67
+ }
68
+
69
+ traverse(dtcg, []);
70
+ return results;
71
+ }
72
+
73
+ /**
74
+ * @param {{ options: object }} args
75
+ * @returns {string}
76
+ */
77
+ function carbonJsMotionSurfacesFormat({ options }) {
78
+ const outputMode = options?.output ?? 'js'; // 'js' | 'dts'
79
+
80
+ const surfaceList = collectSurfaces();
81
+ // Drop the leading "surface-" prefix from the export name.
82
+ const surfaceNames = surfaceList.map(({ name }) =>
83
+ name.startsWith('surface-') ? name.slice('surface-'.length) : name
84
+ );
85
+
86
+ const lines = [FILE_BANNER];
87
+
88
+ // ── Individual named surface exports ─────────────────────────────────────
89
+ for (let i = 0; i < surfaceList.length; i++) {
90
+ const { description, recipe } = surfaceList[i];
91
+ const exportName = surfaceNames[i];
92
+
93
+ if (description) {
94
+ lines.push(`/** ${description} */`);
95
+ }
96
+ if (outputMode === 'js') {
97
+ lines.push(
98
+ `export const ${exportName} = ${JSON.stringify(recipe, null, 2)};`
99
+ );
100
+ } else {
101
+ lines.push(
102
+ `export declare const ${exportName}: ${toDtsTypeLiteral(recipe)};`
103
+ );
104
+ }
105
+ lines.push('');
106
+ }
107
+
108
+ // ── surfaces map ─────────────────────────────────────────────────────────
109
+ if (outputMode === 'js') {
110
+ lines.push('export const surfaces = {');
111
+ for (const name of surfaceNames) {
112
+ lines.push(` ${name},`);
113
+ }
114
+ lines.push('};');
115
+ lines.push('');
116
+ } else {
117
+ lines.push('export declare const surfaces: {');
118
+ for (const name of surfaceNames) {
119
+ lines.push(` ${name}: typeof ${name};`);
120
+ }
121
+ lines.push('};');
122
+ lines.push('');
123
+ }
124
+
125
+ // ── getMotionSurface ──────────────────────────────────────────────────────
126
+ if (outputMode === 'js') {
127
+ lines.push(
128
+ `export function getMotionSurface(name) {`,
129
+ ` const surface = surfaces[name];`,
130
+ ` if (!surface) {`,
131
+ ` throw new Error(`,
132
+ ` \`Unable to find motion surface \\\`\${name}\\\`. Expected one of: \${Object.keys(surfaces).join(', ')}\``,
133
+ ` );`,
134
+ ` }`,
135
+ ` return surface;`,
136
+ `}`,
137
+ ``
138
+ );
139
+ } else {
140
+ lines.push(
141
+ `export declare function getMotionSurface(name: ${surfaceNames
142
+ .map((n) => `'${n}'`)
143
+ .join(' | ')}): (typeof surfaces)[typeof name];`,
144
+ ``
145
+ );
146
+ }
147
+
148
+ return lines.join('\n');
149
+ }
150
+
151
+ module.exports = {
152
+ name: 'carbon/motion-js-surfaces',
153
+ format: carbonJsMotionSurfacesFormat,
154
+ };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Copyright IBM Corp. 2018, 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
+ * Emits `js/generated/tokens.{js,d.ts}` from the motion DTCG tokens.
12
+ *
13
+ * The format receives all resolved tokens (duration + easing) and produces:
14
+ * - V11 duration exports (durationFast01, durationFast02, …)
15
+ * - V10 deprecated aliases (fast01, fast02, …)
16
+ * - `easings` nested object ({ standard: { productive, expressive }, … })
17
+ * - `unstable_tokens` array
18
+ *
19
+ * Two files are emitted by wiring this format into two SD `files` entries
20
+ * that differ only in `options.output` ('js' | 'dts').
21
+ */
22
+
23
+ const FILE_BANNER = `// Code generated by @carbon/motion. DO NOT EDIT.
24
+ //
25
+ // Copyright IBM Corp. 2018, 2026
26
+ //
27
+ // This source code is licensed under the Apache-2.0 license found in the
28
+ // LICENSE file in the root directory of this source tree.
29
+ `;
30
+
31
+ /**
32
+ * Convert kebab-case to camelCase.
33
+ * @param {string} str
34
+ * @returns {string}
35
+ */
36
+ function kebabToCamel(str) {
37
+ return str.replace(/-([a-z0-9])/gi, (_, ch) => ch.toUpperCase());
38
+ }
39
+
40
+ /**
41
+ * Derive the V10 camelCase alias name from a V11 camelCase name.
42
+ * e.g. 'durationFast01' → 'fast01'
43
+ */
44
+ function toV10CamelName(v11Camel) {
45
+ // Strip the leading 'duration' prefix and lowercase the first char.
46
+ const withoutPrefix = v11Camel.slice('duration'.length);
47
+ return withoutPrefix.charAt(0).toLowerCase() + withoutPrefix.slice(1);
48
+ }
49
+
50
+ /**
51
+ * @param {{ dictionary: import('style-dictionary').Dictionary, options: object }} args
52
+ * @returns {string}
53
+ */
54
+ function carbonJsMotionTokensFormat({ dictionary, options }) {
55
+ const outputMode = options?.output ?? 'js'; // 'js' | 'dts'
56
+
57
+ // Build a lookup from kebab token name → resolved value
58
+ const tokenMap = new Map(
59
+ dictionary.allTokens.map((t) => [t.name, t.value ?? t.$value])
60
+ );
61
+
62
+ const lines = [FILE_BANNER];
63
+
64
+ // ── Duration tokens (V11 canonical) ──────────────────────────────────────
65
+ lines.push('// Duration tokens (V11)');
66
+ const durationTokens = dictionary.allTokens.filter((t) =>
67
+ t.name.startsWith('duration-')
68
+ );
69
+ const durationCamels = [];
70
+
71
+ for (const token of durationTokens) {
72
+ const camel = kebabToCamel(token.name);
73
+ durationCamels.push(camel);
74
+ const value = token.value ?? token.$value;
75
+ const description = token.$description ?? token.description ?? '';
76
+
77
+ if (description) {
78
+ lines.push(`/** ${description} */`);
79
+ }
80
+ if (outputMode === 'js') {
81
+ lines.push(`export const ${camel} = '${value}';`);
82
+ } else {
83
+ lines.push(`export declare const ${camel}: string;`);
84
+ }
85
+ }
86
+
87
+ // ── Duration tokens (V10 deprecated aliases) ──────────────────────────────
88
+ lines.push('');
89
+ lines.push('// Duration tokens (V10 — deprecated aliases)');
90
+ const v10Camels = [];
91
+
92
+ for (const v11Camel of durationCamels) {
93
+ const v10Camel = toV10CamelName(v11Camel);
94
+ v10Camels.push(v10Camel);
95
+
96
+ if (outputMode === 'js') {
97
+ lines.push(`/** @deprecated Use \`${v11Camel}\` instead */`);
98
+ lines.push(`export const ${v10Camel} = ${v11Camel};`);
99
+ } else {
100
+ lines.push(`/** @deprecated Use \`${v11Camel}\` instead */`);
101
+ lines.push(`export declare const ${v10Camel}: string;`);
102
+ }
103
+ }
104
+
105
+ // ── Easing map ────────────────────────────────────────────────────────────
106
+ lines.push('');
107
+ lines.push('// Easing tokens');
108
+
109
+ if (outputMode === 'dts') {
110
+ lines.push(
111
+ "export declare const easings: Record<'standard' | 'entrance' | 'exit', Record<'productive' | 'expressive', string>>;"
112
+ );
113
+ } else {
114
+ lines.push('export const easings = {');
115
+ for (const easingName of ['standard', 'entrance', 'exit']) {
116
+ lines.push(` ${easingName}: {`);
117
+ for (const mode of ['productive', 'expressive']) {
118
+ const key = `easing-${easingName}-${mode}`;
119
+ const value = tokenMap.get(key);
120
+ if (value === undefined) {
121
+ throw new Error(`Missing SD token: ${key}`);
122
+ }
123
+ lines.push(` ${mode}: '${value}',`);
124
+ }
125
+ lines.push(` },`);
126
+ }
127
+ lines.push('};');
128
+ }
129
+
130
+ // ── unstable_tokens ───────────────────────────────────────────────────────
131
+ lines.push('');
132
+ if (outputMode === 'dts') {
133
+ lines.push('export declare const unstable_tokens: readonly string[];');
134
+ } else {
135
+ lines.push('export const unstable_tokens = [');
136
+ for (const camel of durationCamels) lines.push(` '${camel}',`);
137
+ for (const v10 of v10Camels) lines.push(` '${v10}',`);
138
+ lines.push('];');
139
+ }
140
+
141
+ lines.push('');
142
+ return lines.join('\n');
143
+ }
144
+
145
+ module.exports = {
146
+ name: 'carbon/motion-js-tokens',
147
+ format: carbonJsMotionTokensFormat,
148
+ };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Copyright IBM Corp. 2018, 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
+ * Emits `scss/generated/_surfaces.scss` from surfaces.json.
12
+ *
13
+ * Surface data lives in `$extensions["carbon.motion"]` on each leaf token —
14
+ * not in `$value` — so Style Dictionary cannot derive the output from the
15
+ * token value alone. This format reads surfaces.json directly (the same
16
+ * pattern used by `carbon/scss-tokens` in @carbon/themes, which reads Carbon
17
+ * JS metadata directly instead of using SD token values).
18
+ *
19
+ * Generates:
20
+ * $surfaces: (
21
+ * disclosure: ( kind: 'reveal', duration: 'moderate-01', … ),
22
+ * …
23
+ * );
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ const FILE_BANNER = `// Code generated by @carbon/motion. DO NOT EDIT.
30
+ //
31
+ // Copyright IBM Corp. 2018, 2026
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
+ // Path is resolved relative to this file (style-dictionary/formats/) → src/dtcg/
39
+ const SURFACES_PATH = path.resolve(__dirname, '../../src/dtcg/surfaces.json');
40
+
41
+ /**
42
+ * Collect surface entries from surfaces.json.
43
+ *
44
+ * @returns {Array<{ name: string, description: string, recipe: object }>}
45
+ */
46
+ function collectSurfaces() {
47
+ const dtcg = JSON.parse(fs.readFileSync(SURFACES_PATH, 'utf8'));
48
+ const results = [];
49
+
50
+ function traverse(obj, pathSegments) {
51
+ for (const [key, value] of Object.entries(obj)) {
52
+ if (key.startsWith('$')) continue;
53
+ const segments = [...pathSegments, key];
54
+ if (
55
+ value &&
56
+ typeof value === 'object' &&
57
+ value.$extensions &&
58
+ value.$extensions['carbon.motion']
59
+ ) {
60
+ results.push({
61
+ name: segments.join('-'),
62
+ description: value.$description || '',
63
+ recipe: value.$extensions['carbon.motion'],
64
+ });
65
+ } else if (value && typeof value === 'object') {
66
+ traverse(value, segments);
67
+ }
68
+ }
69
+ }
70
+
71
+ traverse(dtcg, []);
72
+ return results;
73
+ }
74
+
75
+ /**
76
+ * Convert a camelCase JS key to kebab-case for Sass.
77
+ * e.g. "enterEasing" → "enter-easing", "blockSize" → "block-size"
78
+ *
79
+ * @param {string} str
80
+ * @returns {string}
81
+ */
82
+ function camelToKebab(str) {
83
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
84
+ }
85
+
86
+ /**
87
+ * Serialise a JS recipe value to a Sass map / list / string literal.
88
+ * Arrays become Sass lists, plain objects become Sass maps, numbers stay
89
+ * numeric, strings get single-quoted.
90
+ *
91
+ * @param {*} value
92
+ * @param {number} [indent=0]
93
+ * @returns {string}
94
+ */
95
+ function toSassLiteral(value, indent = 0) {
96
+ const pad = ' '.repeat(indent);
97
+ const innerPad = ' '.repeat(indent + 1);
98
+
99
+ if (Array.isArray(value)) {
100
+ const items = value.map((v) => toSassLiteral(v, 0)).join(', ');
101
+ return `(${items})`;
102
+ }
103
+
104
+ if (value !== null && typeof value === 'object') {
105
+ const entries = Object.entries(value)
106
+ .map(
107
+ ([k, v]) =>
108
+ `${innerPad}${camelToKebab(k)}: ${toSassLiteral(v, indent + 1)}`
109
+ )
110
+ .join(',\n');
111
+ return `(\n${entries},\n${pad})`;
112
+ }
113
+
114
+ if (typeof value === 'number') return String(value);
115
+
116
+ // string — single-quote it
117
+ return `'${String(value)}'`;
118
+ }
119
+
120
+ /**
121
+ * @returns {string}
122
+ */
123
+ function carbonScssMotionSurfacesFormat() {
124
+ const surfaceList = collectSurfaces();
125
+ const lines = [FILE_BANNER];
126
+
127
+ lines.push('$surfaces: (');
128
+
129
+ for (const { name, recipe } of surfaceList) {
130
+ // "surface-disclosure" → "disclosure"
131
+ const surfaceName = name.startsWith('surface-')
132
+ ? name.slice('surface-'.length)
133
+ : name;
134
+
135
+ lines.push(` ${surfaceName}: (`);
136
+ for (const [key, value] of Object.entries(recipe)) {
137
+ const sassKey = camelToKebab(key);
138
+ lines.push(` ${sassKey}: ${toSassLiteral(value, 2)},`);
139
+ }
140
+ lines.push(` ),`);
141
+ }
142
+
143
+ lines.push(');');
144
+ lines.push('');
145
+
146
+ return lines.join('\n');
147
+ }
148
+
149
+ module.exports = {
150
+ name: 'carbon/motion-scss-surfaces',
151
+ format: carbonScssMotionSurfacesFormat,
152
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Copyright IBM Corp. 2018, 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
+ * Emits `scss/generated/_tokens.scss` from the motion DTCG tokens.
12
+ *
13
+ * Generates:
14
+ * - `$easings` Sass map (standard/entrance/exit × productive/expressive)
15
+ * - V11 `$duration-*` variables
16
+ * - V10 `$fast-*` / `$moderate-*` / `$slow-*` deprecated aliases
17
+ */
18
+
19
+ const FILE_BANNER = `// Code generated by @carbon/motion. DO NOT EDIT.
20
+ //
21
+ // Copyright IBM Corp. 2018, 2026
22
+ //
23
+ // This source code is licensed under the Apache-2.0 license found in the
24
+ // LICENSE file in the root directory of this source tree.
25
+ //
26
+ `;
27
+
28
+ /**
29
+ * @param {{ dictionary: import('style-dictionary').Dictionary }} args
30
+ * @returns {string}
31
+ */
32
+ function carbonScssMotionTokensFormat({ dictionary }) {
33
+ // Build a lookup from kebab token name → resolved value
34
+ const tokenMap = new Map(
35
+ dictionary.allTokens.map((t) => [t.name, t.value ?? t.$value])
36
+ );
37
+
38
+ const lines = [FILE_BANNER];
39
+
40
+ // ── $easings map ──────────────────────────────────────────────────────────
41
+ lines.push('/// Common component easings');
42
+ lines.push('/// @type Map');
43
+ lines.push('/// @access public');
44
+ lines.push('/// @group @carbon/motion');
45
+ lines.push('$easings: (');
46
+ for (const easingName of ['standard', 'entrance', 'exit']) {
47
+ lines.push(` ${easingName}: (`);
48
+ for (const mode of ['productive', 'expressive']) {
49
+ const key = `easing-${easingName}-${mode}`;
50
+ const value = tokenMap.get(key);
51
+ if (value === undefined) {
52
+ throw new Error(`Missing SD token: ${key}`);
53
+ }
54
+ lines.push(` ${mode}: ${value},`);
55
+ }
56
+ lines.push(` ),`);
57
+ }
58
+ lines.push(') !default;');
59
+ lines.push('');
60
+
61
+ // ── V11 duration variables ────────────────────────────────────────────────
62
+ const durationTokens = dictionary.allTokens.filter((t) =>
63
+ t.name.startsWith('duration-')
64
+ );
65
+
66
+ for (const token of durationTokens) {
67
+ const description = token.$description ?? token.description ?? '';
68
+ if (description) lines.push(`/// ${description}`);
69
+ lines.push('/// @access public');
70
+ lines.push('/// @type Duration');
71
+ lines.push('/// @group @carbon/motion');
72
+ lines.push(`$${token.name}: ${token.value ?? token.$value} !default;`);
73
+ lines.push('');
74
+ }
75
+
76
+ // ── V10 deprecated aliases ────────────────────────────────────────────────
77
+ lines.push('/// V10 backwards compatibility tokens');
78
+ const v10Map = {
79
+ 'duration-fast-01': 'fast-01',
80
+ 'duration-fast-02': 'fast-02',
81
+ 'duration-moderate-01': 'moderate-01',
82
+ 'duration-moderate-02': 'moderate-02',
83
+ 'duration-slow-01': 'slow-01',
84
+ 'duration-slow-02': 'slow-02',
85
+ };
86
+ for (const [v11, v10] of Object.entries(v10Map)) {
87
+ lines.push('/// @access public');
88
+ lines.push('/// @deprecated');
89
+ lines.push('/// @type Duration');
90
+ lines.push('/// @group @carbon/motion');
91
+ lines.push(`$${v10}: $${v11} !default;`);
92
+ lines.push('');
93
+ }
94
+
95
+ return lines.join('\n');
96
+ }
97
+
98
+ module.exports = {
99
+ name: 'carbon/motion-scss-tokens',
100
+ format: carbonScssMotionTokensFormat,
101
+ };
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Copyright IBM Corp. 2018, 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/motion.
12
+ *
13
+ * Wires together the custom plugins in this directory and drives two separate
14
+ * builds:
15
+ *
16
+ * runJs() → js/generated/tokens.{js,d.ts}
17
+ * js/generated/surfaces.{js,d.ts}
18
+ *
19
+ * runScss() → scss/generated/_tokens.scss
20
+ * scss/generated/_surfaces.scss
21
+ */
22
+
23
+ const path = require('path');
24
+ const fs = require('fs');
25
+ const { default: StyleDictionary } = require('style-dictionary');
26
+
27
+ // ── Custom plugins ─────────────────────────────────────────────────────────
28
+ const carbonMotionDuration = require('./transforms/duration');
29
+ const carbonMotionCubicBezier = require('./transforms/cubic-bezier');
30
+ const carbonJsMotionTokens = require('./formats/js-tokens');
31
+ const carbonJsMotionSurfaces = require('./formats/js-surfaces');
32
+ const carbonScssMotionTokens = require('./formats/scss-tokens');
33
+ const carbonScssMotionSurfaces = require('./formats/scss-surfaces');
34
+
35
+ // ── Paths ──────────────────────────────────────────────────────────────────
36
+ const ROOT = path.resolve(__dirname, '..');
37
+ const DTCG_DIR = path.join(ROOT, 'src', 'dtcg');
38
+ const SCSS_GENERATED = path.join(ROOT, 'scss', 'generated');
39
+ const JS_GENERATED = path.join(ROOT, 'js', 'generated');
40
+
41
+ // ── Custom name transform ──────────────────────────────────────────────────
42
+ //
43
+ // Builds a kebab-case name from the token path segments.
44
+ // e.g. ['duration', 'fast', '01'] → 'duration-fast-01'
45
+ // ['easing', 'standard', 'productive'] → 'easing-standard-productive'
46
+ const carbonMotionNameKebab = {
47
+ name: 'carbon/motion-name-kebab',
48
+ type: 'name',
49
+ transform(token) {
50
+ return token.path.join('-');
51
+ },
52
+ };
53
+
54
+ // ── Transform group ────────────────────────────────────────────────────────
55
+ const CARBON_MOTION_TRANSFORMS = [
56
+ 'attribute/cti',
57
+ 'carbon/motion-name-kebab',
58
+ 'carbon/motion-duration',
59
+ 'carbon/motion-cubic-bezier',
60
+ ];
61
+
62
+ // ── SD configs ────────────────────────────────────────────────────────────
63
+ //
64
+ // Separate configs are used for JS and SCSS so that `runJs()` and `runScss()`
65
+ // only invoke their respective platforms, matching the themes pattern where
66
+ // each build step is independently callable.
67
+
68
+ // motion.json — JS output
69
+ const jsTokensConfig = {
70
+ source: [path.join(DTCG_DIR, 'motion.json')],
71
+ platforms: {
72
+ 'js/tokens': {
73
+ transformGroup: 'carbon/motion',
74
+ buildPath: JS_GENERATED + '/',
75
+ files: [
76
+ {
77
+ destination: 'tokens.js',
78
+ format: 'carbon/motion-js-tokens',
79
+ options: { output: 'js' },
80
+ },
81
+ {
82
+ destination: 'tokens.d.ts',
83
+ format: 'carbon/motion-js-tokens',
84
+ options: { output: 'dts' },
85
+ },
86
+ ],
87
+ },
88
+ },
89
+ };
90
+
91
+ // motion.json — SCSS output
92
+ const scssTokensConfig = {
93
+ source: [path.join(DTCG_DIR, 'motion.json')],
94
+ platforms: {
95
+ 'scss/tokens': {
96
+ transformGroup: 'carbon/motion',
97
+ buildPath: SCSS_GENERATED + '/',
98
+ files: [
99
+ {
100
+ destination: '_tokens.scss',
101
+ format: 'carbon/motion-scss-tokens',
102
+ },
103
+ ],
104
+ },
105
+ },
106
+ };
107
+
108
+ // surfaces.json — JS output
109
+ //
110
+ // Surfaces are processed by their formats reading surfaces.json directly;
111
+ // SD still manages the build lifecycle and output directory creation.
112
+ // motion.json is included so SD can resolve the alias references in
113
+ // surfaces.json $value fields (even though the format reads $extensions
114
+ // directly and does not consume the resolved values).
115
+ const jsSurfacesConfig = {
116
+ source: [
117
+ path.join(DTCG_DIR, 'motion.json'),
118
+ path.join(DTCG_DIR, 'surfaces.json'),
119
+ ],
120
+ // The two source files both have a top-level $description; SD reports this
121
+ // as a collision even though it is DTCG metadata, not a real token conflict.
122
+ log: { warnings: 'disabled' },
123
+ platforms: {
124
+ 'js/surfaces': {
125
+ transformGroup: 'carbon/motion',
126
+ buildPath: JS_GENERATED + '/',
127
+ files: [
128
+ {
129
+ destination: 'surfaces.js',
130
+ format: 'carbon/motion-js-surfaces',
131
+ options: { output: 'js' },
132
+ },
133
+ {
134
+ destination: 'surfaces.d.ts',
135
+ format: 'carbon/motion-js-surfaces',
136
+ options: { output: 'dts' },
137
+ },
138
+ ],
139
+ },
140
+ },
141
+ };
142
+
143
+ // surfaces.json — SCSS output
144
+ const scssSurfacesConfig = {
145
+ source: [
146
+ path.join(DTCG_DIR, 'motion.json'),
147
+ path.join(DTCG_DIR, 'surfaces.json'),
148
+ ],
149
+ log: { warnings: 'disabled' },
150
+ platforms: {
151
+ 'scss/surfaces': {
152
+ transformGroup: 'carbon/motion',
153
+ buildPath: SCSS_GENERATED + '/',
154
+ files: [
155
+ {
156
+ destination: '_surfaces.scss',
157
+ format: 'carbon/motion-scss-surfaces',
158
+ },
159
+ ],
160
+ },
161
+ },
162
+ };
163
+
164
+ // ── Build a registered SD instance ────────────────────────────────────────
165
+ // In SD v5, register* methods live on the instance, not the class.
166
+ // We create one base instance with all plugins registered, then extend it
167
+ // per-config so each build inherits the registrations.
168
+ function createBase() {
169
+ const base = new StyleDictionary({});
170
+ base.registerTransform(carbonMotionNameKebab);
171
+ base.registerTransform(carbonMotionDuration);
172
+ base.registerTransform(carbonMotionCubicBezier);
173
+ base.registerTransformGroup({
174
+ name: 'carbon/motion',
175
+ transforms: CARBON_MOTION_TRANSFORMS,
176
+ });
177
+ base.registerFormat(carbonJsMotionTokens);
178
+ base.registerFormat(carbonJsMotionSurfaces);
179
+ base.registerFormat(carbonScssMotionTokens);
180
+ base.registerFormat(carbonScssMotionSurfaces);
181
+ return base;
182
+ }
183
+
184
+ // ── JS build ──────────────────────────────────────────────────────────────
185
+ // Generates:
186
+ // js/generated/tokens.{js,d.ts}
187
+ // js/generated/surfaces.{js,d.ts}
188
+ async function runJs() {
189
+ fs.mkdirSync(JS_GENERATED, { recursive: true });
190
+ const base = createBase();
191
+ await (await base.extend(jsTokensConfig)).buildAllPlatforms();
192
+ await (await base.extend(jsSurfacesConfig)).buildAllPlatforms();
193
+ }
194
+
195
+ // ── SCSS build ────────────────────────────────────────────────────────────
196
+ // Generates:
197
+ // scss/generated/_tokens.scss
198
+ // scss/generated/_surfaces.scss
199
+ async function runScss() {
200
+ fs.mkdirSync(SCSS_GENERATED, { recursive: true });
201
+ const base = createBase();
202
+ await (await base.extend(scssTokensConfig)).buildAllPlatforms();
203
+ await (await base.extend(scssSurfacesConfig)).buildAllPlatforms();
204
+ }
205
+
206
+ // ── Full build (JS + SCSS) ─────────────────────────────────────────────────
207
+ async function run() {
208
+ await runJs();
209
+ await runScss();
210
+ }
211
+
212
+ module.exports = {
213
+ run,
214
+ runJs,
215
+ runScss,
216
+ jsTokensConfig,
217
+ scssTokensConfig,
218
+ jsSurfacesConfig,
219
+ scssSurfacesConfig,
220
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Copyright IBM Corp. 2018, 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
+ * Converts a DTCG cubicBezier $value array `[x1, y1, x2, y2]` into the CSS
12
+ * string `"cubic-bezier(x1, y1, x2, y2)"`.
13
+ *
14
+ * Style Dictionary v5 does not perform this conversion for cubicBezier tokens
15
+ * automatically, so we handle it here.
16
+ *
17
+ * Examples:
18
+ * [0.2, 0, 0.38, 0.9] → 'cubic-bezier(0.2, 0, 0.38, 0.9)'
19
+ * [0, 0, 0.3, 1] → 'cubic-bezier(0, 0, 0.3, 1)'
20
+ */
21
+ module.exports = {
22
+ name: 'carbon/motion-cubic-bezier',
23
+ type: 'value',
24
+ transitive: true,
25
+ filter(token) {
26
+ return token.$type === 'cubicBezier' || token.type === 'cubicBezier';
27
+ },
28
+ transform(token) {
29
+ const v = token.value !== undefined ? token.value : token.$value;
30
+
31
+ if (Array.isArray(v) && v.length === 4) {
32
+ return `cubic-bezier(${v.join(', ')})`;
33
+ }
34
+
35
+ // Already a resolved string — pass through.
36
+ return v;
37
+ },
38
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Copyright IBM Corp. 2018, 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
+ * Converts a DTCG duration $value object `{ value: N, unit: "ms" }` into the
12
+ * CSS string `"Nms"`.
13
+ *
14
+ * The DTCG spec stores duration as a composite object rather than a bare
15
+ * string; Style Dictionary does not know this shape natively, so we resolve
16
+ * it here before the value reaches any format.
17
+ *
18
+ * Examples:
19
+ * { value: 70, unit: 'ms' } → '70ms'
20
+ * { value: 400, unit: 'ms' } → '400ms'
21
+ */
22
+ module.exports = {
23
+ name: 'carbon/motion-duration',
24
+ type: 'value',
25
+ transitive: true,
26
+ filter(token) {
27
+ return token.$type === 'duration' || token.type === 'duration';
28
+ },
29
+ transform(token) {
30
+ const v = token.value !== undefined ? token.value : token.$value;
31
+
32
+ if (v && typeof v === 'object' && 'value' in v && 'unit' in v) {
33
+ return `${v.value}${v.unit}`;
34
+ }
35
+
36
+ // Already a resolved string (e.g. from a transitive pass) — pass through.
37
+ return v;
38
+ },
39
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Copyright IBM Corp. 2018, 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
+ * Serialise a JS value as a TypeScript type literal.
12
+ * Preserves string/number literal types (and nested object/tuple structure)
13
+ * so generated `.d.ts` files discriminate on `kind` and keep token names
14
+ * assignable to hand-authored unions like `DurationName`.
15
+ *
16
+ * Examples:
17
+ * 'reveal' → '"reveal"'
18
+ * 0 → '0'
19
+ * { kind: 'reveal' } → '{\n kind: "reveal"\n}'
20
+ * ['entrance', 'productive'] → '[\n "entrance",\n "productive"\n]'
21
+ *
22
+ * @param {*} value
23
+ * @param {number} [indent=0]
24
+ * @returns {string}
25
+ */
26
+ function toDtsTypeLiteral(value, indent = 0) {
27
+ const pad = ' '.repeat(indent);
28
+ const innerPad = ' '.repeat(indent + 1);
29
+
30
+ if (Array.isArray(value)) {
31
+ if (value.length === 0) return '[]';
32
+ const items = value
33
+ .map((item) => `${innerPad}${toDtsTypeLiteral(item, indent + 1)}`)
34
+ .join(',\n');
35
+ return `[\n${items}\n${pad}]`;
36
+ }
37
+
38
+ if (value !== null && typeof value === 'object') {
39
+ const entries = Object.entries(value);
40
+ if (entries.length === 0) return '{}';
41
+ const body = entries
42
+ .map(
43
+ ([key, nested]) =>
44
+ `${innerPad}${key}: ${toDtsTypeLiteral(nested, indent + 1)}`
45
+ )
46
+ .join(',\n');
47
+ return `{\n${body}\n${pad}}`;
48
+ }
49
+
50
+ if (typeof value === 'number' || typeof value === 'boolean') {
51
+ return String(value);
52
+ }
53
+
54
+ if (value === null) return 'null';
55
+
56
+ // string literal type
57
+ return JSON.stringify(String(value));
58
+ }
59
+
60
+ module.exports = { toDtsTypeLiteral };