@massimo-cassandro/minimo 1.33.6 → 1.34.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.
Files changed (19) hide show
  1. package/design-tokens/README.md +2 -197
  2. package/dev-tools/README.md +1 -0
  3. package/dev-tools/design-tokens-builder/README.md +208 -0
  4. package/dev-tools/design-tokens-builder/build-tokens-src/build-source-modes.mjs +152 -0
  5. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/config.mjs +35 -1
  6. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/formats/css.mjs +16 -4
  7. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/formats/json.mjs +9 -4
  8. package/dev-tools/design-tokens-builder/build-tokens-src/merge-css.mjs +192 -0
  9. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/platforms.mjs +3 -1
  10. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens.mjs +99 -45
  11. package/{design-tokens/utilities → dev-tools/design-tokens-builder}/check-unresolved-custom-props.mjs +66 -13
  12. package/{design-tokens → dev-tools/design-tokens-builder}/tokens-config-sample.mjs +39 -1
  13. package/dev-tools/starter-kit/source_files/root/package-tpl.json +2 -2
  14. package/dev-tools/starter-kit/starter-install.sh +2 -2
  15. package/package.json +6 -8
  16. package/src/web-components/s-datatable-component/src/parse-cols.js +1 -1
  17. package/design-tokens/utilities/build-tokens-src/merge-css.mjs +0 -60
  18. /package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/resolve-source-paths.mjs +0 -0
  19. /package/{design-tokens/utilities → dev-tools/design-tokens-builder}/build-tokens-src/transforms.mjs +0 -0
@@ -6,201 +6,6 @@
6
6
 
7
7
  <!-- TODO modalità d'uso (da tokens penpots o figma, con o snza merge css ecc...) -->
8
8
 
9
- ## Design Token Utilities
9
+ ## Design Tokens Builder
10
10
 
11
- <!-- TODO rivedere e semplificare -->
12
-
13
- A set of Node.js scripts for managing CSS design tokens in a project. Built on top of [Style Dictionary v5](https://styledictionary.com/).
14
-
15
- ### Scripts
16
-
17
- | Script | Purpose |
18
- |---|---|
19
- | `build-tokens.mjs` | Generates a CSS custom properties file from design token source files |
20
- | `check-unresolved-custom-props.mjs` | Scans CSS files and reports any `var(--...)` references not found in the generated token file |
21
-
22
- Both scripts share the same config file, passed via `--config`.
23
-
24
- ### Setup
25
-
26
- The scripts are intended to be used as npm scripts. Add them to your `package.json`:
27
-
28
- ```json
29
- {
30
- "scripts": {
31
- "build tokens": "npx buildTokens --config ./path/to/tokens-config.mjs",
32
- "check unresolved props": "npx checkUnresolvedProps --config ./path/to/tokens-config.mjs"
33
- }
34
- }
35
- ```
36
-
37
- ### Dependencies
38
-
39
- The following packages must be present in your project:
40
-
41
- ```json
42
- {
43
- "style-dictionary": "^5",
44
- "stylelint": "^16"
45
- }
46
- ```
47
-
48
- ### Configuration file
49
-
50
- Both scripts read a shared config file passed with `--config`. Use `tokens-config-sample.mjs` as a starting point.
51
-
52
- Read comments inside the sample file for more info.
53
-
54
-
55
- ### `build-tokens.mjs`
56
-
57
- Reads design token source files and generates:
58
- - A **CSS file** containing all tokens as custom properties inside `:root { ... }`
59
- - Optionally, one or more **W3C DTCG JSON/JSONC** files, independent of any specific consuming tool (usable in Penpot, Figma via plugin, Token Studio, ...).
60
-
61
- #### Usage
62
-
63
- ```sh
64
- npx buildTokens --config ./path/to/tokens-config.mjs
65
- ```
66
-
67
- #### Source files
68
-
69
- The `source` array accepts any combination of:
70
- - Concrete file paths: `'./tokens/colors.jsonc'`
71
- - Glob patterns: `'./tokens/**/*.{json,jsonc,mjs}'`
72
-
73
- **Supported formats:**
74
- - `.json` / `.jsonc` — parsed directly as token data
75
- - `.mjs` / `.js` — imported as ES modules; must export a W3C DTCG-compliant object as `default`
76
-
77
-
78
- #### Token file format (JS example)
79
-
80
- When using `.mjs` source files, export a default object following the [W3C Design Token Community Group format](https://design-tokens.github.io/community-group/format/):
81
-
82
- ```js
83
- // colors.tokens.mjs
84
- export default {
85
- color: {
86
- brand: {
87
- primary: { $type: 'color', $value: '#0057FF' },
88
- secondary: { $type: 'color', $value: '#FF6B00' },
89
- },
90
- neutral: {
91
- '100': { $type: 'color', $value: '#F5F5F5' },
92
- '900': { $type: 'color', $value: '#1A1A1A' },
93
- },
94
- },
95
- semantic: {
96
- color: {
97
- text: {
98
- primary: { $type: 'color', $value: '{color.neutral.900}' }, // alias
99
- },
100
- },
101
- },
102
- };
103
- ```
104
-
105
- The main advantage of `.mjs` over `.json` is the ability to use JavaScript variables, comments, and computed values within the token definition.
106
-
107
- Values can be expressions like `"{size.base} * .25"`, see `tokens-config-sample.mjs` for more info.
108
-
109
- #### Custom transforms
110
-
111
- The following transforms are registered on top of Style Dictionary's built-ins:
112
-
113
- | Name | Type | Description |
114
- |---|---|---|
115
- | `shadow/css` | value | Converts shadow objects (single or array) to a `box-shadow` CSS string. px values are converted to rem. |
116
- | `size/pxToRem-smart` | value | Converts `dimension` token values from px to rem. Skips values already in rem. |
117
- | `color/css-modern` | value | Color passthrough with transitive alias support. |
118
- | `gradient/css` | value | Converts gradient objects to CSS `linear-gradient`, `radial-gradient` or `conic-gradient`. |
119
- | `composite/css` | value | Converts `border`, `outline`, `transition` and `animation` objects to their CSS shorthand. |
120
- | `typography/css` | value | Typography passthrough — the format handles exploding the value into individual properties. |
121
-
122
- #### CSS output
123
-
124
- All tokens are output as CSS custom properties inside `:root`, sorted alphabetically.
125
-
126
- Typography tokens are expanded into multiple properties:
127
- - `--token-name-font` — a `font` shorthand (when both `fontSize` and `fontFamily` are present)
128
- - `--token-name-letter-spacing`, `--token-name-text-transform`, `--token-name-text-decoration` — individual properties
129
-
130
- The generated file is linted and auto-fixed with stylelint using the config specified in `stylelintConfigPath`.
131
-
132
- #### JSON output
133
-
134
- When `jsonBuildPath` is set, the script also generates W3C DTCG token files, independent of any specific consuming tool (e.g. importable into Penpot via the [Design Tokens plugin](https://penpot.app/penpot-files/design-tokens), into Figma via a compatible plugin, or into Token Studio).
135
-
136
- **Single file** (`jsonDestFile` is a string):
137
- ```
138
- jsonBuildPath/tokens.jsonc ← all tokens in one file
139
- ```
140
-
141
- **One file per source** (`jsonDestFile` is `null` or omitted):
142
- ```
143
- jsonBuildPath/colors.tokens.jsonc
144
- jsonBuildPath/spacing.tokens.jsonc
145
- jsonBuildPath/typography.tokens.jsonc
146
- ```
147
- All files are written flat in `jsonBuildPath`, regardless of the subdirectory structure of the source files — no subdirectories are created. If two source files with different paths would produce the same output filename, the build fails with an error listing the colliding files instead of silently overwriting one with the other.
148
-
149
- The `jsonFormat` option controls the output:
150
-
151
- | Value | Extension | Disclaimer header |
152
- |---|---|---|
153
- | `'jsonc'` | `.jsonc` | Yes |
154
- | `'json'` | `.json` | No |
155
-
156
- Alias references (`{color.brand.primary}`) are preserved in the output so consuming tools maintain token links between primitive and semantic tokens.
157
-
158
- > JSON token files intentionally use a minimal set of transforms (no px→rem conversion, no shorthand expansion) because consuming tools generally expect original values and structured objects, not resolved CSS strings.
159
-
160
- ---
161
-
162
- ### `check-unresolved-custom-props.mjs`
163
-
164
- Scans all `.css` files in `dirToCheck` (recursively) and reports:
165
-
166
- - **unresolved properties** — `var(--...)` references that are not defined in the generated token CSS file or in any `extraCustomPropsFiles`
167
- - **unused properties** — custom properties defined in the generated token CSS file (or an `extraCustomPropsFiles` entry) but never referenced via `var()` in any scanned CSS file. Only reported if `checkUnused: true` is set in the config, since it re-runs the Style Dictionary transform pipeline over the token sources to resolve, when possible, the `.mjs` source file for each unused property. When `ignoreUnusedInNodeModules` is true (default), properties sourced from a token file under `node_modules` (e.g. a third-party token package) are never reported as unused.
168
-
169
- Results are written to `unresolved-unused-props.md` in the same directory as the config file.
170
-
171
- #### Usage
172
-
173
- ```sh
174
- npx checkUnresolvedProps --config ./path/to/tokens-config.mjs
175
- ```
176
-
177
- #### Output
178
-
179
- The script creates a `unresolved-unused-props.md` file next to the config file (only if there is at least one unresolved or unused property). Each unresolved entry links to the file and line number where the property is used; each unused entry links to its `.mjs` token source file when it can be resolved, otherwise just the property name is reported:
180
-
181
- ```markdown
182
- ## Unresolved custom properties
183
-
184
- * [components.css](../src/components.css#L42) -> `--color-action-hover`
185
- * [layout.css](../src/layout.css#L17) -> `--spacing-missing`
186
-
187
- ## Unused custom properties
188
-
189
- * [colors.minimo.tokens.mjs](../_src/colors.minimo.tokens.mjs) -> `--color-legacy-accent`
190
- * `--manually-added-prop`
191
- ```
192
-
193
- In VS Code, Option/Alt-clicking on the URL opens the file at the required line.
194
-
195
-
196
- #### Excluding properties
197
-
198
- Use `excludePattern` to suppress false positives — for example, internal/private custom properties that are defined dynamically or inline:
199
-
200
- ```js
201
- // skips all custom props starting with `--_` or `--js-`
202
- excludePattern: [
203
- /^--_/,
204
- /^--js-/,
205
- ]
206
- ```
11
+ The Node.js scripts that generate the CSS custom properties / JSON token files from the sources in this directory (`build-tokens.mjs`, `check-unresolved-custom-props.mjs`, exposed as the `buildTokens` / `checkUnresolvedProps` bins) live in [dev-tools/design-tokens-builder](../dev-tools/design-tokens-builder/README.md), alongside the other standalone dev-tools CLI utilities.
@@ -6,6 +6,7 @@ A collection of standalone Node.js CLI utilities to help manage a web project's
6
6
  * [Svg Icons Tools](./svg-icons-tools/README.md) — optimizes SVG icons and combines/converts them into symbols, JSX, or SCSS variables
7
7
  * [Create favicons](./create-favicons/README.md) — generates the full favicon set (SVG, PNG, ICO, webmanifest, HTML snippet) from a source image
8
8
  * [upd@m](./upd@m/README.md) — updates all `@massimo-cassandro/*` packages installed in `node_modules`
9
+ * [Design Tokens Builder](./design-tokens-builder/README.md) — generates a CSS custom properties file (and optional JSON token files) from design token sources
9
10
  * [starter-kit](#starter-kit) — script for installing minimo environment
10
11
 
11
12
  ## starter-kit
@@ -0,0 +1,208 @@
1
+ # Design Tokens Builder
2
+
3
+ <!-- TODO rivedere e semplificare -->
4
+
5
+ A set of Node.js scripts for managing CSS design tokens in a project. Built on top of [Style Dictionary v5](https://styledictionary.com/).
6
+
7
+ ### Scripts
8
+
9
+ | Script | Purpose |
10
+ |---|---|
11
+ | `build-tokens.mjs` | Generates a CSS custom properties file from design token source files |
12
+ | `check-unresolved-custom-props.mjs` | Scans CSS files and reports any `var(--...)` references not found in the generated token file |
13
+
14
+ Both scripts share the same config file, passed via `--config`.
15
+
16
+ ### Setup
17
+
18
+ The scripts are intended to be used as npm scripts. Add them to your `package.json`:
19
+
20
+ ```json
21
+ {
22
+ "scripts": {
23
+ "build tokens": "npx buildTokens --config ./path/to/tokens-config.mjs",
24
+ "check unresolved props": "npx checkUnresolvedProps --config ./path/to/tokens-config.mjs"
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### Dependencies
30
+
31
+ The following packages must be present in your project:
32
+
33
+ ```json
34
+ {
35
+ "style-dictionary": "^5",
36
+ "stylelint": "^16"
37
+ }
38
+ ```
39
+
40
+ ### Configuration file
41
+
42
+ Both scripts read a shared config file passed with `--config`. Use `tokens-config-sample.mjs` as a starting point.
43
+
44
+ Read comments inside the sample file for more info.
45
+
46
+
47
+ ### `build-tokens.mjs`
48
+
49
+ Reads design token source files and generates:
50
+ - A **CSS file** containing all tokens as custom properties inside `:root { ... }`
51
+ - Optionally, one or more **W3C DTCG JSON/JSONC** files, independent of any specific consuming tool (usable in Penpot, Figma via plugin, Token Studio, ...).
52
+
53
+ #### Usage
54
+
55
+ ```sh
56
+ npx buildTokens --config ./path/to/tokens-config.mjs
57
+ ```
58
+
59
+ #### Source files
60
+
61
+ The `source` array accepts any combination of:
62
+ - Concrete file paths: `'./tokens/colors.jsonc'`
63
+ - Glob patterns: `'./tokens/**/*.{json,jsonc,mjs}'`
64
+
65
+ **Supported formats:**
66
+ - `.json` / `.jsonc` — parsed directly as token data
67
+ - `.mjs` / `.js` — imported as ES modules; must export a W3C DTCG-compliant object as `default`
68
+
69
+
70
+ #### Token file format (JS example)
71
+
72
+ When using `.mjs` source files, export a default object following the [W3C Design Token Community Group format](https://design-tokens.github.io/community-group/format/):
73
+
74
+ ```js
75
+ // colors.tokens.mjs
76
+ export default {
77
+ color: {
78
+ brand: {
79
+ primary: { $type: 'color', $value: '#0057FF' },
80
+ secondary: { $type: 'color', $value: '#FF6B00' },
81
+ },
82
+ neutral: {
83
+ '100': { $type: 'color', $value: '#F5F5F5' },
84
+ '900': { $type: 'color', $value: '#1A1A1A' },
85
+ },
86
+ },
87
+ semantic: {
88
+ color: {
89
+ text: {
90
+ primary: { $type: 'color', $value: '{color.neutral.900}' }, // alias
91
+ },
92
+ },
93
+ },
94
+ };
95
+ ```
96
+
97
+ The main advantage of `.mjs` over `.json` is the ability to use JavaScript variables, comments, and computed values within the token definition.
98
+
99
+ Values can be expressions like `"{size.base} * .25"`, see `tokens-config-sample.mjs` for more info.
100
+
101
+ #### Custom transforms
102
+
103
+ The following transforms are registered on top of Style Dictionary's built-ins:
104
+
105
+ | Name | Type | Description |
106
+ |---|---|---|
107
+ | `shadow/css` | value | Converts shadow objects (single or array) to a `box-shadow` CSS string. px values are converted to rem. |
108
+ | `size/pxToRem-smart` | value | Converts `dimension` token values from px to rem. Skips values already in rem. |
109
+ | `color/css-modern` | value | Color passthrough with transitive alias support. |
110
+ | `gradient/css` | value | Converts gradient objects to CSS `linear-gradient`, `radial-gradient` or `conic-gradient`. |
111
+ | `composite/css` | value | Converts `border`, `outline`, `transition` and `animation` objects to their CSS shorthand. |
112
+ | `typography/css` | value | Typography passthrough — the format handles exploding the value into individual properties. |
113
+
114
+ #### CSS output
115
+
116
+ All tokens are output as CSS custom properties inside `:root`, sorted alphabetically.
117
+
118
+ Typography tokens are expanded into multiple properties:
119
+ - `--token-name-font` — a `font` shorthand (when both `fontSize` and `fontFamily` are present)
120
+ - `--token-name-letter-spacing`, `--token-name-text-transform`, `--token-name-text-decoration` — individual properties
121
+
122
+ The generated file is linted and auto-fixed with stylelint using the config specified in `stylelintConfigPath`.
123
+
124
+ #### JSON output
125
+
126
+ When `jsonBuildPath` is set, the script also generates W3C DTCG token files, independent of any specific consuming tool (e.g. importable into Penpot via the [Design Tokens plugin](https://penpot.app/penpot-files/design-tokens), into Figma via a compatible plugin, or into Token Studio).
127
+
128
+ **Single file** (`jsonDestFile` is a string):
129
+ ```
130
+ jsonBuildPath/tokens.jsonc ← all tokens in one file
131
+ ```
132
+
133
+ **One file per source** (`jsonDestFile` is `null` or omitted):
134
+ ```
135
+ jsonBuildPath/colors.tokens.jsonc
136
+ jsonBuildPath/spacing.tokens.jsonc
137
+ jsonBuildPath/typography.tokens.jsonc
138
+ ```
139
+ All files are written flat in `jsonBuildPath`, regardless of the subdirectory structure of the source files — no subdirectories are created. If two source files with different paths would produce the same output filename, the build fails with an error listing the colliding files instead of silently overwriting one with the other.
140
+
141
+ The `jsonFormat` option controls the output:
142
+
143
+ | Value | Extension | Disclaimer header |
144
+ |---|---|---|
145
+ | `'jsonc'` | `.jsonc` | Yes |
146
+ | `'json'` | `.json` | No |
147
+
148
+ Alias references (`{color.brand.primary}`) are preserved in the output so consuming tools maintain token links between primitive and semantic tokens.
149
+
150
+ > JSON token files intentionally use a minimal set of transforms (no px→rem conversion, no shorthand expansion) because consuming tools generally expect original values and structured objects, not resolved CSS strings.
151
+
152
+ #### sourceModes (light/dark split)
153
+
154
+ `sourceModes` is an alternative to `source`, for projects that need a light/dark (or other) custom properties split. See the comments in `tokens-config-sample.mjs` for the full option reference (`sourceModes`, `sourceModesBase`).
155
+
156
+ When set, each mode has its own `source` array, and the generated CSS composes all modes into a single `destFile`:
157
+ - the base mode's props go in a top-level `:root { ... }` block, with `color-scheme: <all modes>;` prepended
158
+ - every other mode's props go in `@media (prefers-color-scheme: <mode>) { :root { ... } }`, with `color-scheme: <mode>;` prepended
159
+
160
+ `customPropsGroups`, `mergeCustomProps`, `addLayer`, `pxToRem` etc. all apply the same way as with a single `source`. The JSON output (when `jsonBuildPath` is set) is produced once per mode, with the mode name appended to each filename (e.g. `tokens-light.jsonc` / `tokens-dark.jsonc`).
161
+
162
+ ---
163
+
164
+ ### `check-unresolved-custom-props.mjs`
165
+
166
+ Scans all `.css` files in `dirToCheck` (recursively) and reports:
167
+
168
+ - **unresolved properties** — `var(--...)` references that are not defined in the generated token CSS file or in any `extraCustomPropsFiles`
169
+ - **unused properties** — custom properties defined in the generated token CSS file (or an `extraCustomPropsFiles` entry) but never referenced via `var()` in any scanned CSS file. Only reported if `checkUnused: true` is set in the config, since it re-runs the Style Dictionary transform pipeline over the token sources to resolve, when possible, the `.mjs` source file for each unused property. When `ignoreUnusedInNodeModules` is true (default), properties sourced from a token file under `node_modules` (e.g. a third-party token package) are never reported as unused.
170
+
171
+ Results are written to `unresolved-unused-props.md` in the same directory as the config file.
172
+
173
+ #### Usage
174
+
175
+ ```sh
176
+ npx checkUnresolvedProps --config ./path/to/tokens-config.mjs
177
+ ```
178
+
179
+ #### Output
180
+
181
+ The script creates a `unresolved-unused-props.md` file next to the config file (only if there is at least one unresolved or unused property). Each unresolved entry links to the file and line number where the property is used; each unused entry links to its `.mjs` token source file when it can be resolved, otherwise just the property name is reported:
182
+
183
+ ```markdown
184
+ ## Unresolved custom properties
185
+
186
+ * [components.css](../src/components.css#L42) -> `--color-action-hover`
187
+ * [layout.css](../src/layout.css#L17) -> `--spacing-missing`
188
+
189
+ ## Unused custom properties
190
+
191
+ * [colors.minimo.tokens.mjs](../_src/colors.minimo.tokens.mjs) -> `--color-legacy-accent`
192
+ * `--manually-added-prop`
193
+ ```
194
+
195
+ In VS Code, Option/Alt-clicking on the URL opens the file at the required line.
196
+
197
+
198
+ #### Excluding properties
199
+
200
+ Use `excludePattern` to suppress false positives — for example, internal/private custom properties that are defined dynamically or inline:
201
+
202
+ ```js
203
+ // skips all custom props starting with `--_` or `--js-`
204
+ excludePattern: [
205
+ /^--_/,
206
+ /^--js-/,
207
+ ]
208
+ ```
@@ -0,0 +1,152 @@
1
+ // build-tokens-src/build-source-modes.mjs
2
+ // Alternative build path used when the project config sets `sourceModes`
3
+ // (see config.mjs) instead of a single `source` array — typically for a
4
+ // light/dark custom-properties split.
5
+ //
6
+ // For each mode, a separate StyleDictionary instance is built from that
7
+ // mode's own `source` array. The generated `:root { ... }` CSS blocks are
8
+ // then composed into a single destFile:
9
+ // - the base mode's block stays as a plain `:root { ... }` rule, with a
10
+ // `color-scheme: <all modes joined>;` declaration prepended
11
+ // (e.g. "light dark")
12
+ // - every other mode's block is wrapped in
13
+ // `@media (prefers-color-scheme: <mode>) { :root { ... } }`, with a
14
+ // `color-scheme: <mode>;` declaration prepended
15
+ // customPropsGroups, mergeCustomProps and pxToRem apply per mode, exactly as
16
+ // in the single-source build. addLayer (if set) wraps the whole composed
17
+ // result in a single `@layer`, rather than one per mode.
18
+ //
19
+ // JSON tokens (if jsonBuildPath is set) are produced per mode: one set of
20
+ // files per mode, with `-<mode>` appended to each filename (see
21
+ // buildJsonFiles() in formats/json.mjs), e.g. "tokens-light.jsonc" /
22
+ // "tokens-dark.jsonc", or "size-light.jsonc" / "size-dark.jsonc" in
23
+ // multi-file mode (jsonDestFile: null).
24
+
25
+ import StyleDictionary from 'style-dictionary';
26
+ import * as path from 'node:path';
27
+ import { mkdir, writeFile } from 'node:fs/promises';
28
+ import { CSS_TRANSFORMS, JSON_TRANSFORMS } from './platforms.mjs';
29
+ import { customPropsCount } from './formats/css.mjs';
30
+ import { buildJsonFiles, collectConcreteFilePaths } from './formats/json.mjs';
31
+ import { loadExistingCustomPropsScoped } from './merge-css.mjs';
32
+
33
+ /**
34
+ * @param {object} opts
35
+ * @param {Record<string,string[]>} opts.sourceModes Resolved source paths per mode (see config.mjs)
36
+ * @param {string} opts.baseMode Mode whose block stays a top-level `:root { ... }` rule
37
+ * @param {string} opts.buildPath Absolute path for CSS output directory
38
+ * @param {string} opts.destFile CSS output filename
39
+ * @param {string|null} opts.jsonBuildPath Absolute path for JSON output (null = disabled)
40
+ * @param {string|null} opts.jsonDestFile Base name for aggregated JSON file; null = one file per source
41
+ * @param {'json'|'jsonc'} opts.jsonFormat Output format for JSON files
42
+ * @param {'keep'|'calc'|'resolve'} opts.jsonExpression How to handle math expressions in dimension tokens
43
+ * @param {{name:string,prefixes:string[]}[]} opts.customPropsGroups
44
+ * @param {boolean} opts.pxToRem
45
+ * @param {string|null} opts.addLayer
46
+ * @param {boolean} opts.mergeCustomProps
47
+ * @returns {Promise<{cssDestPath: string, totalCustomProps: number, jsonFilesByMode: Record<string, object[]>}>}
48
+ */
49
+ export const buildSourceModes = async ({
50
+ sourceModes,
51
+ baseMode,
52
+ buildPath,
53
+ destFile,
54
+ jsonBuildPath,
55
+ jsonDestFile,
56
+ jsonFormat,
57
+ jsonExpression,
58
+ customPropsGroups,
59
+ pxToRem,
60
+ addLayer,
61
+ mergeCustomProps,
62
+ }) => {
63
+ const modeNames = Object.keys(sourceModes);
64
+
65
+ // Read the pre-existing destFile (if any) BEFORE any Style Dictionary
66
+ // instance runs, since the first one to write would overwrite it.
67
+ if (mergeCustomProps) {
68
+ loadExistingCustomPropsScoped(path.join(buildPath, destFile), baseMode);
69
+ }
70
+
71
+ const cssTransforms = pxToRem
72
+ ? CSS_TRANSFORMS
73
+ : CSS_TRANSFORMS.filter((name) => name !== 'size/pxToRem-smart');
74
+
75
+ /** @type {Record<string,string>} */
76
+ const modeBlocks = {};
77
+ let totalCustomProps = 0;
78
+ /** @type {Record<string, object[]>} */
79
+ const jsonFilesByMode = {};
80
+
81
+ for (const mode of modeNames) {
82
+ const sd = new StyleDictionary({
83
+ source: sourceModes[mode],
84
+ log: { verbosity: 'verbose' },
85
+ platforms: {
86
+ css: {
87
+ transforms: cssTransforms,
88
+ files: [
89
+ {
90
+ destination: destFile,
91
+ format: 'css/variables-sorted',
92
+ options: {
93
+ outputReferences: true,
94
+ customPropsGroups,
95
+ mode,
96
+ colorScheme: mode === baseMode ? modeNames.join(' ') : mode,
97
+ },
98
+ },
99
+ ],
100
+ },
101
+ },
102
+ });
103
+
104
+ // formatPlatform() runs the format function without writing to disk —
105
+ // the composed multi-mode file is written once, at the end, below.
106
+ const [formatted] = await sd.formatPlatform('css');
107
+ modeBlocks[mode] = /** @type {string} */ (formatted.output);
108
+ // customPropsCount is a live binding, updated synchronously by the
109
+ // format function called above (see formats/css.mjs).
110
+ totalCustomProps += customPropsCount;
111
+
112
+ if (jsonBuildPath) {
113
+ const concreteFilePaths = jsonDestFile ? [] : await collectConcreteFilePaths(sd);
114
+ const files = buildJsonFiles(concreteFilePaths, jsonDestFile, jsonFormat, jsonExpression, `-${mode}`);
115
+
116
+ const jsonSd = new StyleDictionary({
117
+ source: sourceModes[mode],
118
+ log: { verbosity: 'silent' },
119
+ platforms: {
120
+ json: {
121
+ buildPath: jsonBuildPath + '/',
122
+ transforms: JSON_TRANSFORMS,
123
+ files,
124
+ },
125
+ },
126
+ });
127
+ await jsonSd.buildAllPlatforms();
128
+ jsonFilesByMode[mode] = files;
129
+ }
130
+ }
131
+
132
+ // ── Compose the final CSS ────────────────────────────────────────────────
133
+ const otherModes = modeNames.filter((mode) => mode !== baseMode);
134
+
135
+ let composed = modeBlocks[baseMode];
136
+ for (const mode of otherModes) {
137
+ composed += `\n@media (prefers-color-scheme: ${mode}) {\n${modeBlocks[mode]}}\n`;
138
+ }
139
+
140
+ // addLayer wraps the whole composed result once, rather than once per
141
+ // mode. Indentation is left to stylelint's fix step (run right after the
142
+ // build), same as the single-source build.
143
+ const finalCss = addLayer
144
+ ? `@layer ${addLayer} {\n\n${composed}}\n`
145
+ : composed;
146
+
147
+ await mkdir(buildPath, { recursive: true });
148
+ const cssDestPath = path.join(buildPath, destFile);
149
+ await writeFile(cssDestPath, finalCss);
150
+
151
+ return { cssDestPath, totalCustomProps, jsonFilesByMode };
152
+ };
@@ -110,5 +110,39 @@ export const customPropsGroups = Array.isArray(buildConfig.customPropsGroups)
110
110
  // path.join() — it may corrupt the pattern syntax. Use template literals:
111
111
  // OK: `${minimo_path}/**/*.{json,mjs}`
112
112
  // NO: path.join(minimo_path, '/**/*.{json,mjs}')
113
+ //
114
+ // sourceModes: alternative to `source`, for a light/dark (or other) custom
115
+ // properties split — see build-tokens-src/build-source-modes.mjs. When set
116
+ // (buildConfig.sourceModes is a non-null object), `source` is ignored and
117
+ // each mode's own array of source patterns is resolved the same way.
113
118
  // ---------------------------------------------------------------------------
114
- export const source = resolveSourcePaths(buildConfig.source, configDir);
119
+ const sourceModesRaw = buildConfig.sourceModes ?? null;
120
+
121
+ export const sourceModes = sourceModesRaw
122
+ ? Object.fromEntries(
123
+ Object.entries(sourceModesRaw).map(([mode, modeSource]) => [
124
+ mode,
125
+ resolveSourcePaths(modeSource, configDir),
126
+ ])
127
+ )
128
+ : null;
129
+
130
+ // sourceModesBase: the mode whose declarations are written to the top-level
131
+ // `:root { ... }` block (the others are nested under
132
+ // `@media (prefers-color-scheme: <mode>) { ... }`). Default: the first key
133
+ // of sourceModes (insertion order), e.g. 'light'. Ignored when sourceModes
134
+ // is not set.
135
+ export const sourceModesBase = sourceModes
136
+ ? (Object.prototype.hasOwnProperty.call(sourceModes, buildConfig.sourceModesBase)
137
+ ? buildConfig.sourceModesBase
138
+ : Object.keys(sourceModes)[0])
139
+ : null;
140
+
141
+ if (sourceModes && buildConfig.sourceModesBase && sourceModesBase !== buildConfig.sourceModesBase) {
142
+ // eslint-disable-next-line no-console
143
+ console.warn(
144
+ `[build-tokens] config: sourceModesBase "${buildConfig.sourceModesBase}" is not a key of sourceModes — falling back to "${sourceModesBase}"`
145
+ );
146
+ }
147
+
148
+ export const source = sourceModes ? null : resolveSourcePaths(buildConfig.source, configDir);
@@ -252,11 +252,14 @@ StyleDictionary.registerFormat({
252
252
 
253
253
  // Build a name → declaration-tail map from the declarations generated
254
254
  // above, then merge with any pre-existing custom properties loaded via
255
- // loadExistingCustomProps() (see ../merge-css.mjs). Pre-existing values
256
- // (including trailing comments) take priority when both exist;
257
- // pre-existing-only properties are kept.
255
+ // loadExistingCustomProps() / loadExistingCustomPropsScoped() (see
256
+ // ../merge-css.mjs). Pre-existing values (including trailing comments)
257
+ // take priority when both exist; pre-existing-only properties are kept.
258
+ // options.mode (sourceModes build only, see ../build-source-modes.mjs)
259
+ // scopes the merge to that mode, so same-named props with different
260
+ // values across modes are never mixed up.
258
261
  const generatedProps = parseCustomProps(lines.join('\n'));
259
- const finalProps = mergeCustomProps(generatedProps);
262
+ const finalProps = mergeCustomProps(generatedProps, options.mode ?? null);
260
263
 
261
264
  // Final output is always sorted alphabetically ascending by property
262
265
  // name, regardless of merge — this also keeps pre-existing-only
@@ -295,6 +298,15 @@ StyleDictionary.registerFormat({
295
298
  // first hyphen-separated segment (e.g. all `btn-*` together), to visually
296
299
  // group related custom properties in the generated file.
297
300
  const outLines = [];
301
+
302
+ // options.colorScheme (sourceModes build only, see ../build-source-modes.mjs):
303
+ // prepends a `color-scheme: <value>;` declaration, e.g. "light dark" for
304
+ // the base mode's block, or the mode's own name (e.g. "dark") for a
305
+ // block nested inside its @media (prefers-color-scheme: <mode>) rule.
306
+ if (options.colorScheme) {
307
+ outLines.push(` color-scheme: ${options.colorScheme};`, '');
308
+ }
309
+
298
310
  let prevPrefix = null;
299
311
  const appendBlock = (props) => {
300
312
  for (const [name, tail] of props) {