@massimo-cassandro/minimo 0.3.6 → 0.4.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 (99) hide show
  1. package/README.md +1 -1
  2. package/design-tokens/alerts.minimo.tokens.jsonc +28 -0
  3. package/design-tokens/badge.minimo.tokens.jsonc +42 -0
  4. package/design-tokens/basic.minimo.tokens.jsonc +58 -0
  5. package/design-tokens/btn-close.minimo.tokens.jsonc +96 -0
  6. package/design-tokens/btn-primary.minimo.tokens.jsonc +96 -0
  7. package/design-tokens/btn.minimo.tokens.jsonc +196 -0
  8. package/design-tokens/color-accent.minimo.tokens.jsonc +18 -0
  9. package/design-tokens/color-neutral.minimo.tokens.jsonc +18 -0
  10. package/design-tokens/color-primary.minimo.tokens.jsonc +18 -0
  11. package/design-tokens/color-secondary.minimo.tokens.jsonc +18 -0
  12. package/design-tokens/components/recaptcha.minimo.tokens.jsonc +28 -0
  13. package/design-tokens/components/sf-macro.minimo.tokens.jsonc +66 -0
  14. package/design-tokens/components/spinner.minimo.tokens.jsonc +24 -0
  15. package/design-tokens/components/toast.minimo.tokens.jsonc +32 -0
  16. package/design-tokens/flex-grid.minimo.tokens.jsonc +24 -0
  17. package/design-tokens/form.minimo.tokens.jsonc +304 -0
  18. package/design-tokens/headings.minimo.tokens.jsonc +62 -0
  19. package/design-tokens/link.minimo.tokens.jsonc +44 -0
  20. package/design-tokens/radius.minimo.tokens.jsonc +42 -0
  21. package/design-tokens/recaptcha.minimo.tokens.jsonc +28 -0
  22. package/design-tokens/sf-macro.minimo.tokens.jsonc +66 -0
  23. package/design-tokens/simple-datatable.minimo.tokens.jsonc +51 -0
  24. package/design-tokens/size.minimo.tokens.jsonc +46 -0
  25. package/design-tokens/spinner.minimo.tokens.jsonc +24 -0
  26. package/design-tokens/status.minimo.tokens.jsonc +98 -0
  27. package/design-tokens/table.minimo.tokens.jsonc +64 -0
  28. package/design-tokens/toast.minimo.tokens.jsonc +32 -0
  29. package/design-tokens/typography.minimo.tokens.jsonc +102 -0
  30. package/design-tokens/utility.minimo.tokens.jsonc +14 -0
  31. package/design-tokens/web-components/simple-datatable.minimo.tokens.jsonc +51 -0
  32. package/design-tokens/z-index.minimo.tokens.jsonc +14 -0
  33. package/design-tokens-src/_readme.md +27 -0
  34. package/design-tokens-src/alerts.minimo.tokens.mjs +25 -0
  35. package/design-tokens-src/size.minimo.tokens.jsonc +105 -0
  36. package/design-tokens-utilities/build-tokens-src/config.mjs +108 -0
  37. package/design-tokens-utilities/build-tokens-src/formats/css.mjs +201 -0
  38. package/design-tokens-utilities/build-tokens-src/formats/penpot.mjs +262 -0
  39. package/design-tokens-utilities/build-tokens-src/platforms.mjs +80 -0
  40. package/design-tokens-utilities/build-tokens-src/transforms.mjs +200 -0
  41. package/design-tokens-utilities/build-tokens.mjs +114 -0
  42. package/design-tokens-utilities/check-unresolved-custom-props.mjs +137 -0
  43. package/design-tokens-utilities/readme.md +185 -0
  44. package/design-tokens-utilities/tokens-config-sample.mjs +98 -0
  45. package/package.json +10 -6
  46. package/src/components/{overlay.js → overlay/overlay.js} +1 -1
  47. package/src/{css/dialog-content.css → components/popup-page/popup-page.css} +1 -1
  48. package/src/components/{dialog-content.js → popup-page/popup-page.js} +6 -4
  49. package/src/custom-properties-default.css +5 -5
  50. package/src/icons/x.svg +1 -0
  51. package/src/web-components/simple-datatable/simple-datatable.js +2 -2
  52. package/src/web-components/simple-datatable/simple-datatable.module.css +3 -3
  53. package/src/web-components/simple-datatable/src/parse-cols.js +2 -2
  54. package/src/design-tokens/TODO.md +0 -2
  55. package/src/design-tokens/alerts.minimo.tokens.jsonc +0 -25
  56. package/src/design-tokens/size.minimo.tokens.jsonc +0 -101
  57. package/src/design-tokens-utilities/build-tokens.mjs +0 -503
  58. package/src/design-tokens-utilities/check-unresolved-custom-props.mjs +0 -131
  59. package/src/design-tokens-utilities/config-templates/tokens-config.mjs +0 -34
  60. package/src/design-tokens-utilities/readme.md +0 -12
  61. /package/{src/design-tokens → design-tokens-src}/badge.minimo.tokens.jsonc +0 -0
  62. /package/{src/design-tokens → design-tokens-src}/basic.minimo.tokens.jsonc +0 -0
  63. /package/{src/design-tokens → design-tokens-src}/btn-close.minimo.tokens.jsonc +0 -0
  64. /package/{src/design-tokens → design-tokens-src}/btn-primary.minimo.tokens.jsonc +0 -0
  65. /package/{src/design-tokens → design-tokens-src}/btn-secondary.minimo.tokens.jsonc +0 -0
  66. /package/{src/design-tokens → design-tokens-src}/btn.minimo.tokens.jsonc +0 -0
  67. /package/{src/design-tokens → design-tokens-src}/color-accent.minimo.tokens.jsonc +0 -0
  68. /package/{src/design-tokens → design-tokens-src}/color-neutral.minimo.tokens.jsonc +0 -0
  69. /package/{src/design-tokens → design-tokens-src}/color-primary.minimo.tokens.jsonc +0 -0
  70. /package/{src/design-tokens → design-tokens-src}/color-secondary.minimo.tokens.jsonc +0 -0
  71. /package/{src/components/recaptcha → design-tokens-src/components}/recaptcha.minimo.tokens.jsonc +0 -0
  72. /package/{src/components/sf-macro → design-tokens-src/components}/sf-macro.minimo.tokens.jsonc +0 -0
  73. /package/{src/components/spinner → design-tokens-src/components}/spinner.minimo.tokens.jsonc +0 -0
  74. /package/{src/components/toast → design-tokens-src/components}/toast.minimo.tokens.jsonc +0 -0
  75. /package/{src/design-tokens → design-tokens-src}/flex-grid.minimo.tokens.jsonc +0 -0
  76. /package/{src/design-tokens → design-tokens-src}/form.minimo.tokens.jsonc +0 -0
  77. /package/{src/design-tokens → design-tokens-src}/gradients-shadowes.minimo.tokens.jsonc +0 -0
  78. /package/{src/design-tokens → design-tokens-src}/headings.minimo.tokens.jsonc +0 -0
  79. /package/{src/design-tokens → design-tokens-src}/link.minimo.tokens.jsonc +0 -0
  80. /package/{src/design-tokens → design-tokens-src}/radius.minimo.tokens.jsonc +0 -0
  81. /package/{src/design-tokens → design-tokens-src}/status.minimo.tokens.jsonc +0 -0
  82. /package/{src/design-tokens → design-tokens-src}/table.minimo.tokens.jsonc +0 -0
  83. /package/{src/design-tokens → design-tokens-src}/typography.minimo.tokens.jsonc +0 -0
  84. /package/{src/design-tokens → design-tokens-src}/utility.minimo.tokens.jsonc +0 -0
  85. /package/{src/web-components/simple-datatable → design-tokens-src/web-components}/simple-datatable.minimo.tokens.jsonc +0 -0
  86. /package/{src/design-tokens → design-tokens-src}/z-index.minimo.tokens.jsonc +0 -0
  87. /package/src/{css → components/overlay}/overlay.css +0 -0
  88. /package/src/{web-components/simple-datatable/icons → icons}/arrow-down.svg +0 -0
  89. /package/src/{web-components/simple-datatable/icons → icons}/arrow-up.svg +0 -0
  90. /package/src/{web-components/simple-datatable/icons → icons}/arrows-down-up.svg +0 -0
  91. /package/src/{web-components/simple-datatable/icons → icons}/caret-left.svg +0 -0
  92. /package/src/{web-components/simple-datatable/icons → icons}/caret-right.svg +0 -0
  93. /package/src/{web-components/simple-datatable/icons → icons}/check-bold.svg +0 -0
  94. /package/src/{components/modal-alert/svg → icons}/check-circle-duotone.svg +0 -0
  95. /package/src/{components/modal-alert/svg → icons}/info-duotone.svg +0 -0
  96. /package/src/{components/modal-alert/svg → icons}/question-duotone.svg +0 -0
  97. /package/src/{components/modal-alert/svg → icons}/warning-duotone.svg +0 -0
  98. /package/src/{web-components/simple-datatable/icons → icons}/x-bold.svg +0 -0
  99. /package/src/{components/modal-alert/svg → icons}/x-circle-duotone.svg +0 -0
@@ -0,0 +1,200 @@
1
+ // build-tokens-src/transforms.mjs
2
+ // Registers all custom Style Dictionary transforms.
3
+ // Imported once by build-tokens.mjs before buildAllPlatforms() is called.
4
+
5
+ import StyleDictionary from 'style-dictionary';
6
+
7
+ const BASE_FONT_SIZE = 16;
8
+
9
+ // Converts a px value to rem. Returns the original string unchanged for
10
+ // non-px values, and '0' for zero values regardless of unit.
11
+ const toRem = (val) => {
12
+ if (val === undefined || val === null) return '0';
13
+ const str = String(val).trim();
14
+ const num = parseFloat(str);
15
+ if (isNaN(num)) return str;
16
+ if (num === 0) return '0';
17
+ if (str.endsWith('px')) return `${num / BASE_FONT_SIZE}rem`;
18
+ return str;
19
+ };
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // 1. Shadow
23
+ // Converts shadow token objects (single or array) to a CSS box-shadow string.
24
+ // px values are converted to rem. Alias references are resolved transitively.
25
+ // ---------------------------------------------------------------------------
26
+ StyleDictionary.registerTransform({
27
+ name: 'shadow/css',
28
+ type: 'value',
29
+ transitive: true,
30
+ filter: (token) => token.$type === 'shadow' || token.type === 'shadow',
31
+ transform: (token) => {
32
+ const original = token.original?.$value ?? token.original?.value;
33
+
34
+ const value = (typeof original === 'string' && original.startsWith('{'))
35
+ ? (token.$value ?? token.value)
36
+ : (original ?? token.$value ?? token.value);
37
+
38
+ const shadows = Array.isArray(value) ? value : [value];
39
+
40
+ // Already a resolved CSS string (e.g. from an alias)
41
+ if (shadows.length === 1 && typeof shadows[0] === 'string') return shadows[0];
42
+
43
+ return shadows
44
+ .map((s) => {
45
+ const offsetX = toRem(s.offsetX ?? s.x);
46
+ const offsetY = toRem(s.offsetY ?? s.y);
47
+ const blur = toRem(s.blur);
48
+ const spread = toRem(s.spread);
49
+ const color = s.color ?? 'transparent';
50
+ const inset = s.inset ? 'inset ' : '';
51
+ return `${inset}${offsetX} ${offsetY} ${blur} ${spread} ${color}`;
52
+ })
53
+ .join(', ');
54
+ },
55
+ });
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // 2. px → rem (dimension tokens only, skips values already in rem)
59
+ // ---------------------------------------------------------------------------
60
+ StyleDictionary.registerTransform({
61
+ name: 'size/pxToRem-smart',
62
+ type: 'value',
63
+ filter: (token) => {
64
+ const value = token.$value || token.value;
65
+ return (token.$type === 'dimension' || token.type === 'dimension')
66
+ && typeof value === 'string'
67
+ && !value.endsWith('rem');
68
+ },
69
+ transform: (token) => {
70
+ const value = token.$value || token.value;
71
+ const floatValue = parseFloat(value);
72
+ if (isNaN(floatValue)) return value;
73
+ return `${floatValue / BASE_FONT_SIZE}rem`;
74
+ },
75
+ });
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // 3. Color passthrough
79
+ // Ensures the resolved value is returned as-is, supporting both $value and
80
+ // legacy value fields and transitive alias resolution.
81
+ // ---------------------------------------------------------------------------
82
+ StyleDictionary.registerTransform({
83
+ name: 'color/css-modern',
84
+ type: 'value',
85
+ transitive: true,
86
+ filter: (token) => token.$type === 'color' || token.type === 'color',
87
+ transform: (token) => token.$value ?? token.value,
88
+ });
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // 4. Gradient
92
+ // Converts gradient token objects to CSS gradient functions.
93
+ // Supports linear, radial and conic types. Alias references are resolved
94
+ // transitively; already-resolved strings are returned unchanged.
95
+ // ---------------------------------------------------------------------------
96
+ StyleDictionary.registerTransform({
97
+ name: 'gradient/css',
98
+ type: 'value',
99
+ transitive: true,
100
+ filter: (token) => token.$type === 'gradient' || token.type === 'gradient',
101
+ transform: (token) => {
102
+ const original = token.original?.$value ?? token.original?.value;
103
+
104
+ const value = (typeof original === 'string' && original.startsWith('{'))
105
+ ? (token.$value ?? token.value)
106
+ : (original ?? token.$value ?? token.value);
107
+
108
+ if (typeof value === 'string') return value;
109
+
110
+ const { type = 'linear', angle = 90, stops = [] } = value;
111
+
112
+ const stopsList = stops
113
+ .map((s) => {
114
+ let color = s.color ?? 'transparent';
115
+ if (s.alpha !== undefined) {
116
+ color = `color-mix(in srgb, ${color} ${s.alpha * 100}%, transparent)`;
117
+ }
118
+ const position = s.position !== undefined ? ` ${s.position * 100}%` : '';
119
+ return `${color}${position}`;
120
+ })
121
+ .join(', ');
122
+
123
+ if (type === 'linear') return `linear-gradient(${angle}deg, ${stopsList})`;
124
+ if (type === 'radial') return `radial-gradient(circle, ${stopsList})`;
125
+ if (type === 'conic') return `conic-gradient(from ${angle}deg, ${stopsList})`;
126
+
127
+ return `linear-gradient(${angle}${typeof angle === 'number' ? 'deg' : ''}, ${stopsList})`;
128
+ },
129
+ });
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // 5. Composite (border, outline, transition, animation)
133
+ // Converts composite token objects to their CSS shorthand equivalents.
134
+ // Only applied to object values; strings pass through unchanged.
135
+ // ---------------------------------------------------------------------------
136
+ StyleDictionary.registerTransform({
137
+ name: 'composite/css',
138
+ type: 'value',
139
+ transitive: true,
140
+ filter: (token) => {
141
+ const type = token.$type ?? token.type;
142
+ const value = token.$value ?? token.value;
143
+ return (
144
+ typeof value === 'object' &&
145
+ value !== null &&
146
+ !Array.isArray(value) &&
147
+ ['border', 'outline', 'transition', 'animation'].includes(type)
148
+ );
149
+ },
150
+ transform: (token) => {
151
+ const type = token.$type ?? token.type;
152
+ const v = token.$value ?? token.value;
153
+
154
+ if (type === 'border') {
155
+ const parts = [v.width ?? '1px'];
156
+ if (v.style !== undefined && v.style !== null && v.style !== '') parts.push(v.style);
157
+ parts.push(v.color ?? 'transparent');
158
+ return parts.join(' ');
159
+ }
160
+
161
+ if (type === 'outline') {
162
+ return `${v.width ?? '1px'} ${v.style ?? 'solid'} ${v.color ?? 'transparent'}`;
163
+ }
164
+
165
+ if (type === 'transition') {
166
+ return [
167
+ v.duration ?? '0s',
168
+ v.timingFunction ?? 'ease',
169
+ v.delay ?? '0s',
170
+ v.property ?? 'all',
171
+ ].join(' ');
172
+ }
173
+
174
+ if (type === 'animation') {
175
+ return [
176
+ v.duration ?? '0s',
177
+ v.timingFunction ?? 'ease',
178
+ v.delay ?? '0s',
179
+ v.iterationCount ?? '1',
180
+ v.direction ?? 'normal',
181
+ v.fillMode ?? 'none',
182
+ v.playState ?? 'running',
183
+ v.name ?? 'none',
184
+ ].join(' ');
185
+ }
186
+ },
187
+ });
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // 6. Typography passthrough
191
+ // Returns the token value as-is. The css/variables-sorted format is
192
+ // responsible for exploding typography tokens into individual CSS properties.
193
+ // ---------------------------------------------------------------------------
194
+ StyleDictionary.registerTransform({
195
+ name: 'typography/css',
196
+ type: 'value',
197
+ transitive: true,
198
+ filter: (token) => token.$type === 'typography' || token.type === 'typography',
199
+ transform: (token) => token.$value ?? token.value,
200
+ });
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ // build-tokens.mjs
4
+ // Entry point: imports modules, runs the build, lints the CSS output, prints the log.
5
+ //
6
+ // Project structure:
7
+ // build-tokens.mjs <- this file
8
+ // build-tokens-src/config.mjs <- reads --config flag and resolves all paths
9
+ // build-tokens-src/transforms.mjs <- registers custom Style Dictionary transforms
10
+ // build-tokens-src/formats/css.mjs <- registers the css/variables-sorted format
11
+ // build-tokens-src/formats/penpot.mjs <- registers the json/penpot format, exports helpers
12
+ // build-tokens-src/platforms.mjs <- builds the platforms object (css + optional penpot)
13
+
14
+ import StyleDictionary from 'style-dictionary';
15
+ import path from 'path';
16
+ import { styleText } from 'node:util';
17
+ import { homedir } from 'os';
18
+ import stylelint from 'stylelint';
19
+ import { pathToFileURL } from 'url';
20
+
21
+ // ── 1. Configuration (args + resolved paths) ────────────────────────────────
22
+ import {
23
+ configAbsPath,
24
+ buildPath,
25
+ destFile,
26
+ stylelintConfigPath,
27
+ penpotBuildPath,
28
+ penpotDestFile,
29
+ penpotFormat,
30
+ penpotExpressions,
31
+ source,
32
+ } from './build-tokens-src/config.mjs';
33
+
34
+ // ── 2. Custom transforms ─────────────────────────────────────────────────────
35
+ import './build-tokens-src/transforms.mjs';
36
+
37
+ // ── 3. CSS format ─────────────────────────────────────────────────────────────
38
+ // customPropsCount is updated by the format at render time
39
+ import { customPropsCount } from './build-tokens-src/formats/css.mjs';
40
+
41
+ // ── 4. Penpot format ──────────────────────────────────────────────────────────
42
+ import { buildPenpotFiles, collectConcreteFilePaths } from './build-tokens-src/formats/penpot.mjs';
43
+
44
+ // ── 5. Platforms builder ──────────────────────────────────────────────────────
45
+ import { buildPlatforms } from './build-tokens-src/platforms.mjs';
46
+
47
+ // ── 6. Collect concrete source file paths (multi-file Penpot mode only) ──────
48
+ // Source entries may be glob patterns. SD expands them internally and stores
49
+ // the concrete path in token.filePath. We need those concrete paths to build
50
+ // the per-file descriptors for the penpot platform.
51
+ //
52
+ // SD v5 lazy-loads sources: `await sd.hasInitialized` triggers loading;
53
+ // after that sd.allTokens is a plain synchronous array with filePath populated.
54
+ //
55
+ // In single-file mode (penpotDestFile is set) we skip this step entirely.
56
+ let concreteFilePaths = [];
57
+
58
+ if (penpotBuildPath && !penpotDestFile) {
59
+ const sdInit = new StyleDictionary({ source, log: { verbosity: 'silent' }, platforms: {} });
60
+ concreteFilePaths = await collectConcreteFilePaths(sdInit);
61
+ }
62
+
63
+ // ── 7. Build ──────────────────────────────────────────────────────────────────
64
+ const sd = new StyleDictionary({
65
+ source,
66
+ log: { verbosity: 'verbose' },
67
+ platforms: buildPlatforms({
68
+ buildPath,
69
+ destFile,
70
+ penpotBuildPath,
71
+ penpotDestFile,
72
+ penpotFormat,
73
+ penpotExpressions,
74
+ concreteFilePaths,
75
+ }),
76
+ });
77
+
78
+ await sd.buildAllPlatforms();
79
+
80
+ // ── 8. CSS lint ───────────────────────────────────────────────────────────────
81
+ const stylelintConfig = await import(pathToFileURL(stylelintConfigPath)).then(m => m.default);
82
+
83
+ await stylelint.lint({
84
+ config: stylelintConfig,
85
+ files: [path.join(buildPath, destFile)],
86
+ fix: true,
87
+ });
88
+
89
+ // ── 9. Log ────────────────────────────────────────────────────────────────────
90
+ const short = (p) => p.replace(homedir(), '~');
91
+
92
+ console.log(styleText(['yellow'], `[build-tokens] config file : ${short(configAbsPath)}`));
93
+ console.log(styleText(['yellow'], `[build-tokens] source : ${source.map(short)}`));
94
+ console.log(styleText(['yellow'], `[build-tokens] dest file : ${short(path.join(buildPath, destFile))}`));
95
+
96
+ if (penpotBuildPath) {
97
+ const penpotFiles = buildPenpotFiles(concreteFilePaths, penpotDestFile, penpotFormat, penpotExpressions);
98
+ if (penpotFiles.length === 1) {
99
+ console.log(styleText(['yellow'],
100
+ `[build-tokens] penpot json : ${short(path.join(penpotBuildPath, penpotFiles[0].destination))}`
101
+ ));
102
+ } else {
103
+ console.log(styleText(['yellow'],
104
+ `[build-tokens] penpot json : ${penpotFiles.length} files in ${short(penpotBuildPath)}`
105
+ ));
106
+ }
107
+ } else {
108
+ console.log(styleText(['yellow'],
109
+ `[build-tokens] penpot json : (disabled — penpotBuildPath not set)`
110
+ ));
111
+ }
112
+
113
+ console.log(styleText(['green'], `[build-tokens] ${customPropsCount} custom properties processed`));
114
+ console.log(styleText(['green'], '**** DONE ****'));
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+
3
+ /* eslint-disable no-console */
4
+
5
+ // check-unresolved-custom-props.mjs
6
+ // Scans CSS files in the configured directory and reports any custom property
7
+ // references (var(--...)) that are not defined in the generated token file.
8
+ //
9
+ // Usage: node check-unresolved-custom-props.mjs --config ./path/to/config.mjs
10
+
11
+ import path from 'path';
12
+ import fs from 'node:fs';
13
+ import { fileURLToPath, pathToFileURL } from 'url';
14
+ import { dirname } from 'path';
15
+ import { glob } from 'node:fs/promises';
16
+ import { styleText, parseArgs } from 'node:util';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // 1. Parse flags
23
+ // ---------------------------------------------------------------------------
24
+ const options = {
25
+ config: {
26
+ type: 'string',
27
+ short: 'c',
28
+ },
29
+ };
30
+
31
+ const { values } = parseArgs({ options });
32
+
33
+ if (!values.config) {
34
+ console.error(styleText(['red'], 'Error: you must provide the config path with --config or -c'));
35
+ console.log('Example: node check-unresolved-custom-props.mjs --config ./config.mjs');
36
+ process.exit(1);
37
+ }
38
+
39
+ const absoluteConfigPath = path.resolve(process.cwd(), values.config);
40
+ const configDir = path.dirname(absoluteConfigPath);
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // 2. Main
44
+ // ---------------------------------------------------------------------------
45
+ async function run() {
46
+ try {
47
+ const { default: config } = await import(pathToFileURL(absoluteConfigPath).href);
48
+
49
+ // Path of the generated CSS file containing all custom property definitions
50
+ const custom_prop_file_path = path.resolve(configDir, path.join(config.buildPath, config.destFile));
51
+ const custom_prop_filename = path.basename(custom_prop_file_path);
52
+
53
+ // Directory to scan for CSS files
54
+ const checkdir = path.resolve(configDir, config.dir_to_check);
55
+
56
+ // Patterns for custom properties to exclude from the unresolved check
57
+ const exclude = config.exclude_pattern ?? [];
58
+
59
+ // Collect all .css files in the scan directory, excluding the token file itself
60
+ const files = [];
61
+ for await (const entry of glob(path.join(checkdir, '/**/*.css'))) {
62
+ if (!entry.endsWith(custom_prop_filename)) {
63
+ files.push(entry);
64
+ }
65
+ }
66
+
67
+ if (!fs.existsSync(custom_prop_file_path)) {
68
+ throw new Error(`Token CSS file not found: ${custom_prop_file_path}`);
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // 3. Build the list of defined custom properties
73
+ // ---------------------------------------------------------------------------
74
+ const cssContent = fs.readFileSync(custom_prop_file_path, 'utf-8');
75
+ const definitionRegex = /--([a-z0-9-]+)(?=\s*:)/g;
76
+ const propertyNamesList = [...cssContent.matchAll(definitionRegex)].map(m => `--${m[1]}`);
77
+
78
+ // Also include any extra CSS files declared in extra_custom_props_files
79
+ for (const extraFile of (config.extra_custom_props_files ?? [])) {
80
+ const extraContent = fs.readFileSync(path.resolve(configDir, extraFile), 'utf-8');
81
+ const extraMatches = [...extraContent.matchAll(definitionRegex)].map(m => `--${m[1]}`);
82
+ propertyNamesList.push(...extraMatches);
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // 4. Scan files for unresolved var() references
87
+ // ---------------------------------------------------------------------------
88
+ const usageRegex = /var\(\s*(--[a-z0-9-]+)\s*(,.*?)?\)/g;
89
+ const unresolved_props = [];
90
+
91
+ files.forEach(file => {
92
+ const fileContent = fs.readFileSync(file, 'utf-8');
93
+ const filename = path.relative(configDir, file);
94
+ const matches = [...fileContent.matchAll(usageRegex)];
95
+
96
+ matches.forEach(match => {
97
+ const cprop = match[1];
98
+ const isExcluded = exclude.some(exRegex => exRegex.test(cprop));
99
+ const isDefined = propertyNamesList.includes(cprop);
100
+ const lineNumber = fileContent.substring(0, match.index).split('\n').length;
101
+
102
+ if (!isExcluded && !isDefined) {
103
+ unresolved_props.push({ file: filename, line: lineNumber, prop: cprop });
104
+ }
105
+ });
106
+ });
107
+
108
+ unresolved_props.sort((a, b) => a.file.localeCompare(b.file));
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // 5. Write the report
112
+ // ---------------------------------------------------------------------------
113
+ const result_file = path.resolve(configDir, 'unresolved-props.md');
114
+
115
+ console.log(styleText(['green'],
116
+ `${unresolved_props.length} unresolved custom properties found -> ${path.relative(process.cwd(), result_file)}`
117
+ ));
118
+
119
+ fs.writeFileSync(
120
+ result_file,
121
+ unresolved_props
122
+ .map(item =>
123
+ `* [${path.basename(item.file)} :: ${item.line}](${item.file}#L${item.line}) -> var(${item.prop})`
124
+ )
125
+ .join('\n'),
126
+ 'utf-8'
127
+ );
128
+
129
+ console.log(styleText(['green'], '**** DONE ****'));
130
+
131
+ } catch (err) {
132
+ console.log(styleText(['redBright'], err.stack));
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ run();
@@ -0,0 +1,185 @@
1
+ # Design Token Utilities
2
+
3
+ A set of Node.js scripts for managing CSS design tokens in a project. Built on top of [Style Dictionary v5](https://styledictionary.com/).
4
+
5
+ ## Scripts
6
+
7
+ | Script | Purpose |
8
+ |---|---|
9
+ | `build-tokens.mjs` | Generates a CSS custom properties file from design token source files |
10
+ | `check-unresolved-custom-props.mjs` | Scans CSS files and reports any `var(--...)` references not found in the generated token file |
11
+
12
+ Both scripts share the same config file, passed via `--config`.
13
+
14
+ ## Setup
15
+
16
+ The scripts are intended to be used as npm scripts. Add them to your `package.json`:
17
+
18
+ ```json
19
+ {
20
+ "scripts": {
21
+ "build tokens": "npx buildTokens --config ./path/to/tokens-config.mjs",
22
+ "check unresolved props": "npx checkUnresolvedProps --config ./path/to/tokens-config.mjs"
23
+ }
24
+ }
25
+ ```
26
+
27
+ ### Dependencies
28
+
29
+ The following packages must be present in your project:
30
+
31
+ ```json
32
+ {
33
+ "style-dictionary": "^5",
34
+ "stylelint": "^16"
35
+ }
36
+ ```
37
+
38
+ ## Configuration file
39
+
40
+ Both scripts read a shared config file passed with `--config`. Use `tokens-config-sample.mjs` as a starting point.
41
+
42
+ Read comments inside the sample file for more info.
43
+
44
+
45
+ ## `build-tokens.mjs`
46
+
47
+ Reads design token source files and generates:
48
+ - A **CSS file** containing all tokens as custom properties inside `:root { ... }`
49
+ - Optionally, one or more **W3C DTCG JSON/JSONC** files to import into Penpot/Figma (use in Figma requires a plugin).
50
+
51
+ ### Usage
52
+
53
+ ```sh
54
+ npx buildTokens --config ./path/to/tokens-config.mjs
55
+ ```
56
+
57
+ ### Source files
58
+
59
+ The `source` array accepts any combination of:
60
+ - Concrete file paths: `'./tokens/colors.jsonc'`
61
+ - Glob patterns: `'./tokens/**/*.{json,jsonc,mjs}'`
62
+
63
+ **Supported formats:**
64
+ - `.json` / `.jsonc` — parsed directly as token data
65
+ - `.mjs` / `.js` — imported as ES modules; must export a W3C DTCG-compliant object as `default`
66
+
67
+
68
+ ### Token file format (JS example)
69
+
70
+ 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/):
71
+
72
+ ```js
73
+ // colors.tokens.mjs
74
+ export default {
75
+ color: {
76
+ brand: {
77
+ primary: { $type: 'color', $value: '#0057FF' },
78
+ secondary: { $type: 'color', $value: '#FF6B00' },
79
+ },
80
+ neutral: {
81
+ '100': { $type: 'color', $value: '#F5F5F5' },
82
+ '900': { $type: 'color', $value: '#1A1A1A' },
83
+ },
84
+ },
85
+ semantic: {
86
+ color: {
87
+ text: {
88
+ primary: { $type: 'color', $value: '{color.neutral.900}' }, // alias
89
+ },
90
+ },
91
+ },
92
+ };
93
+ ```
94
+
95
+ The main advantage of `.mjs` over `.json` is the ability to use JavaScript variables, comments, and computed values within the token definition.
96
+
97
+ Values can be expressions like `"{size.base} * .25"`, see `tokens-config-sample.mjs` for more info.
98
+
99
+ ### Custom transforms
100
+
101
+ The following transforms are registered on top of Style Dictionary's built-ins:
102
+
103
+ | Name | Type | Description |
104
+ |---|---|---|
105
+ | `shadow/css` | value | Converts shadow objects (single or array) to a `box-shadow` CSS string. px values are converted to rem. |
106
+ | `size/pxToRem-smart` | value | Converts `dimension` token values from px to rem. Skips values already in rem. |
107
+ | `color/css-modern` | value | Color passthrough with transitive alias support. |
108
+ | `gradient/css` | value | Converts gradient objects to CSS `linear-gradient`, `radial-gradient` or `conic-gradient`. |
109
+ | `composite/css` | value | Converts `border`, `outline`, `transition` and `animation` objects to their CSS shorthand. |
110
+ | `typography/css` | value | Typography passthrough — the format handles exploding the value into individual properties. |
111
+
112
+ ### CSS output
113
+
114
+ All tokens are output as CSS custom properties inside `:root`, sorted alphabetically.
115
+
116
+ Typography tokens are expanded into multiple properties:
117
+ - `--token-name-font` — a `font` shorthand (when both `fontSize` and `fontFamily` are present)
118
+ - `--token-name-letter-spacing`, `--token-name-text-transform`, `--token-name-text-decoration` — individual properties
119
+
120
+ The generated file is linted and auto-fixed with stylelint using the config specified in `stylelintConfigPath`.
121
+
122
+ ### Penpot output
123
+
124
+ When `penpotBuildPath` is set, the script also generates W3C DTCG token files for import into Penpot via the [Design Tokens plugin](https://penpot.app/penpot-files/design-tokens).
125
+
126
+ **Single file** (`penpotDestFile` is a string):
127
+ ```
128
+ penpotBuildPath/tokens.jsonc ← all tokens in one file
129
+ ```
130
+
131
+ **One file per source** (`penpotDestFile` is `null` or omitted):
132
+ ```
133
+ penpotBuildPath/colors.tokens.jsonc
134
+ penpotBuildPath/spacing.tokens.jsonc
135
+ penpotBuildPath/typography.tokens.jsonc
136
+ ```
137
+
138
+ The `penpotFormat` option controls the output:
139
+
140
+ | Value | Extension | Disclaimer header |
141
+ |---|---|---|
142
+ | `'jsonc'` | `.jsonc` | Yes |
143
+ | `'json'` | `.json` | No |
144
+
145
+ Alias references (`{color.brand.primary}`) are preserved in the output so Penpot maintains token links between primitive and semantic tokens.
146
+
147
+ > Penpot token files intentionally use a minimal set of transforms (no px→rem conversion, no shorthand expansion) because Penpot expects original values and structured objects, not resolved CSS strings.
148
+
149
+ ---
150
+
151
+ ## `check-unresolved-custom-props.mjs`
152
+
153
+ Scans all `.css` files in `dir_to_check` (recursively) and reports any `var(--...)` references that are not defined in the generated token CSS file or in any `extra_custom_props_files`.
154
+
155
+ Results are written to `unresolved-props.md` in the same directory as the config file.
156
+
157
+ ### Usage
158
+
159
+ ```sh
160
+ npx checkUnresolvedProps --config ./path/to/tokens-config.mjs
161
+ ```
162
+
163
+ ### Output
164
+
165
+ The script creates a `unresolved-props.md` file next to the config file. Each line links to the file and line number where the unresolved property is used:
166
+
167
+ ```markdown
168
+ * [components.css :: 42](../src/components.css#L42) -> var(--color-action-hover)
169
+ * [layout.css :: 17](../src/layout.css#L17) -> var(--spacing-missing)
170
+ ```
171
+
172
+ In VS Code, Option/Alt-clicking on the URL opens the file at the required line.
173
+
174
+
175
+ ### Excluding properties
176
+
177
+ Use `exclude_pattern` to suppress false positives — for example, internal/private custom properties that are defined dynamically or inline:
178
+
179
+ ```js
180
+ // skips all custom props starting with `--_` or `--js-`
181
+ exclude_pattern: [
182
+ /^--_/,
183
+ /^--js-/,
184
+ ]
185
+ ```