@massimo-cassandro/minimo 1.36.2 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dev-tools/design-tokens-builder/README.md +22 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/build-source-modes.mjs +57 -14
- package/dev-tools/design-tokens-builder/build-tokens-src/config.mjs +42 -3
- package/dev-tools/design-tokens-builder/build-tokens-src/formats/css.mjs +259 -198
- package/dev-tools/design-tokens-builder/build-tokens-src/light-dark.mjs +126 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/merge-css.mjs +25 -14
- package/dev-tools/design-tokens-builder/build-tokens-src/platforms.mjs +7 -2
- package/dev-tools/design-tokens-builder/build-tokens-src/source-prefixes.mjs +129 -0
- package/dev-tools/design-tokens-builder/build-tokens-src/transforms.mjs +20 -0
- package/dev-tools/design-tokens-builder/build-tokens.mjs +14 -0
- package/dev-tools/design-tokens-builder/check-unresolved-custom-props.mjs +13 -8
- package/dev-tools/design-tokens-builder/tokens-config-sample.mjs +55 -4
- package/dev-tools/starter-kit/starter-install.sh +1 -1
- package/package.json +1 -1
|
@@ -155,217 +155,278 @@ const buildGroupTitle = (name) => {
|
|
|
155
155
|
// Exported counter — read by build-tokens.mjs for the final log line
|
|
156
156
|
export let customPropsCount = 0;
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
// Exported live binding holding the name → declaration-tail map last computed
|
|
159
|
+
// by computeFinalProps() (see below), i.e. the structured result of the most
|
|
160
|
+
// recent 'css/variables-sorted' format call. Read by
|
|
161
|
+
// build-tokens-src/build-source-modes.mjs right after each per-mode
|
|
162
|
+
// sd.formatPlatform('css') call (synchronous, same pattern as
|
|
163
|
+
// customPropsCount) — needed by build-tokens-src/light-dark.mjs to reconcile
|
|
164
|
+
// shared light/dark properties into light-dark() calls, working on the
|
|
165
|
+
// structured props instead of the rendered CSS text.
|
|
166
|
+
export let lastFinalProps = {};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Computes the final `name -> declaration-tail` map for a token dictionary:
|
|
170
|
+
* resolves each token's CSS value (including the complex-type builders above),
|
|
171
|
+
* then merges in any pre-existing custom properties via mergeCustomProps()
|
|
172
|
+
* (see ../merge-css.mjs and the `mergeCustomProps` config option).
|
|
173
|
+
* @param {object} dictionary Style Dictionary's resolved token dictionary (format() `dictionary` arg)
|
|
174
|
+
* @param {object} options format() `options` arg — only `outputReferences` and `mode` are read here
|
|
175
|
+
* @returns {Record<string,string>} name → declaration tail (value + terminating `;` + optional trailing comment)
|
|
176
|
+
*/
|
|
177
|
+
export const computeFinalProps = (dictionary, options) => {
|
|
178
|
+
const tokenByPath = {};
|
|
179
|
+
for (const token of dictionary.allTokens) {
|
|
180
|
+
tokenByPath[token.path.join('.')] = token;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const resolveRefs = makeResolveRefs(tokenByPath);
|
|
184
|
+
|
|
185
|
+
const sequenceList = [
|
|
186
|
+
'xxs', 'xs', 'sm', 'base', 'md', 'lg', 'xl', 'xxl', 'xxxl', '2xl', '3xl',
|
|
187
|
+
'xlight', 'light', 'regular', 'medium', 'semibold', 'bold', 'xbold'
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const sortFunction = (a, b) => {
|
|
191
|
+
const idxA = sequenceList.indexOf(a.name);
|
|
192
|
+
const idxB = sequenceList.indexOf(b.name);
|
|
193
|
+
|
|
194
|
+
// Entrambi nella lista → ordine arbitrario
|
|
195
|
+
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
|
196
|
+
|
|
197
|
+
// Solo A nella lista → A viene prima
|
|
198
|
+
if (idxA !== -1) return -1;
|
|
199
|
+
|
|
200
|
+
// Solo B nella lista → B viene prima
|
|
201
|
+
if (idxB !== -1) return 1;
|
|
162
202
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
// Nessuno nella lista → ordine alfabetico
|
|
204
|
+
return a.name.localeCompare(b.name, 'en', { numeric: true, sensitivity: 'base' });
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Tokens loaded via `include` (sourceModes: base-mode tokens made
|
|
208
|
+
// available to other modes) stay in tokenByPath, so references to them
|
|
209
|
+
// resolve to var(--name), but are not declared in this mode's block.
|
|
210
|
+
// property name → token source file, used by mergeCustomProps when the
|
|
211
|
+
// merge is limited to some source files (a token may generate several
|
|
212
|
+
// properties, e.g. typography)
|
|
213
|
+
/** @type {Record<string,string>} */
|
|
214
|
+
const sourceFileMap = {};
|
|
215
|
+
const trackSource = (token, tokenLines) => {
|
|
216
|
+
for (const name of Object.keys(parseCustomProps(tokenLines.join('\n')))) {
|
|
217
|
+
sourceFileMap[name] = token.filePath;
|
|
166
218
|
}
|
|
219
|
+
return tokenLines;
|
|
220
|
+
};
|
|
167
221
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Solo B nella lista → B viene prima
|
|
186
|
-
if (idxB !== -1) return 1;
|
|
187
|
-
|
|
188
|
-
// Nessuno nella lista → ordine alfabetico
|
|
189
|
-
return a.name.localeCompare(b.name, 'en', { numeric: true, sensitivity: 'base' });
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
// Tokens loaded via `include` (sourceModes: base-mode tokens made
|
|
193
|
-
// available to other modes) stay in tokenByPath, so references to them
|
|
194
|
-
// resolve to var(--name), but are not declared in this mode's block.
|
|
195
|
-
// property name → token source file, used by mergeCustomProps when the
|
|
196
|
-
// merge is limited to some source files (a token may generate several
|
|
197
|
-
// properties, e.g. typography)
|
|
198
|
-
/** @type {Record<string,string>} */
|
|
199
|
-
const sourceFileMap = {};
|
|
200
|
-
const trackSource = (token, tokenLines) => {
|
|
201
|
-
for (const name of Object.keys(parseCustomProps(tokenLines.join('\n')))) {
|
|
202
|
-
sourceFileMap[name] = token.filePath;
|
|
222
|
+
const lines = dictionary.allTokens
|
|
223
|
+
.filter((token) => token.isSource !== false)
|
|
224
|
+
// .sort((a, b) => a.name.localeCompare(b.name, 'en', {numeric: true, sensitivity: 'base'}))
|
|
225
|
+
.sort((a, b) => sortFunction(a,b))
|
|
226
|
+
.flatMap((token) => {
|
|
227
|
+
const type = token.$type ?? token.type;
|
|
228
|
+
const orig = token.original?.$value ?? token.original?.value;
|
|
229
|
+
const description = token.$description ?? token.description ?? token.comment;
|
|
230
|
+
const commentSuffix = buildDescriptionComment(description);
|
|
231
|
+
|
|
232
|
+
if (type === 'typography') {
|
|
233
|
+
const typographyLines = buildTypography(token.name, orig ?? {}, resolveRefs)
|
|
234
|
+
.split('\n')
|
|
235
|
+
.map((line) => `${line}${commentSuffix}`)
|
|
236
|
+
.join('\n');
|
|
237
|
+
return trackSource(token, [typographyLines]);
|
|
203
238
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
? resolveRefs(orig) : buildTransition(orig, resolveRefs);
|
|
242
|
-
|
|
243
|
-
} else if (type === 'animation' && options.outputReferences) {
|
|
244
|
-
value = (typeof orig === 'string' && orig.startsWith('{'))
|
|
245
|
-
? resolveRefs(orig) : buildAnimation(orig, resolveRefs);
|
|
246
|
-
|
|
247
|
-
} else if (options.outputReferences && typeof orig === 'string' && orig.includes('{')) {
|
|
248
|
-
// Covers both plain aliases and CSS functions containing {references}
|
|
249
|
-
// e.g. color-mix(in srgb, {btn.secondary.background.color} 60%, #000)
|
|
250
|
-
// {references} are replaced with var(--name); arithmetic gets calc().
|
|
251
|
-
const resolved = resolveRefs(orig);
|
|
252
|
-
// Strip var(...) and CSS function calls (word chars + hyphens followed by '(')
|
|
253
|
-
// before checking for arithmetic operators, so that hyphens in names like
|
|
254
|
-
// "color-mix" or "linear-gradient" don't trigger a spurious calc() wrap.
|
|
255
|
-
const stripped = resolved
|
|
256
|
-
.replace(/var\([^)]+\)/g, '0')
|
|
257
|
-
.replace(/[\w-]+\(/g, '(');
|
|
258
|
-
const isCalcNeeded = /[+\-*/]/.test(stripped);
|
|
259
|
-
value = isCalcNeeded ? `calc(${resolved})` : resolved;
|
|
260
|
-
|
|
261
|
-
} else {
|
|
262
|
-
value = String(token.$value ?? token.value);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Multi-line values (e.g. Open Props' linear() easings) are collapsed
|
|
266
|
-
// to a single line: declarations are parsed one line at a time (see
|
|
267
|
-
// parseCustomProps in ../merge-css.mjs), so a multi-line value would
|
|
268
|
-
// be truncated to its first line.
|
|
269
|
-
value = String(value)
|
|
270
|
-
.replace(/\s*\n\s*/g, ' ')
|
|
271
|
-
.replace(/\(\s+/g, '(')
|
|
272
|
-
.replace(/\s+\)/g, ')');
|
|
273
|
-
|
|
274
|
-
return trackSource(token, [` --${token.name}: ${value};${commentSuffix}`]);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Build a name → declaration-tail map from the declarations generated
|
|
278
|
-
// above, then merge with any pre-existing custom properties loaded via
|
|
279
|
-
// loadExistingCustomProps() / loadExistingCustomPropsScoped() (see
|
|
280
|
-
// ../merge-css.mjs). Pre-existing values (including trailing comments)
|
|
281
|
-
// take priority when both exist; pre-existing-only properties are kept.
|
|
282
|
-
// options.mode (sourceModes build only, see ../build-source-modes.mjs)
|
|
283
|
-
// scopes the merge to that mode, so same-named props with different
|
|
284
|
-
// values across modes are never mixed up.
|
|
285
|
-
const generatedProps = parseCustomProps(lines.join('\n'));
|
|
286
|
-
const finalProps = mergeCustomProps(generatedProps, options.mode ?? null, sourceFileMap);
|
|
287
|
-
|
|
288
|
-
// Final output is always sorted alphabetically ascending by property
|
|
289
|
-
// name, regardless of merge — this also keeps pre-existing-only
|
|
290
|
-
// properties from merge-css.mjs in order instead of trailing at the end.
|
|
291
|
-
const sortedProps = Object.entries(finalProps)
|
|
292
|
-
.sort(([a], [b]) => a.localeCompare(b, 'en', { numeric: true, sensitivity: 'base' }));
|
|
293
|
-
|
|
294
|
-
// customPropsGroups: properties whose first hyphen-separated name segment
|
|
295
|
-
// matches one of a group's prefixes are pulled out, labelled with that
|
|
296
|
-
// group's name and placed, in group-list order, at the beginning of the
|
|
297
|
-
// output file. A property matches the first group (in list order) whose
|
|
298
|
-
// prefixes include it.
|
|
299
|
-
const customPropsGroups = options.customPropsGroups ?? [];
|
|
300
|
-
const groupedProps = customPropsGroups.map(() => []);
|
|
301
|
-
const restProps = [];
|
|
302
|
-
for (const entry of sortedProps) {
|
|
303
|
-
const prefix = entry[0].split('-')[0];
|
|
304
|
-
const groupIndex = customPropsGroups.findIndex((g) => g.prefixes?.includes(prefix));
|
|
305
|
-
if (groupIndex !== -1) {
|
|
306
|
-
groupedProps[groupIndex].push(entry);
|
|
239
|
+
|
|
240
|
+
let value;
|
|
241
|
+
|
|
242
|
+
if (type === 'shadow' && options.outputReferences) {
|
|
243
|
+
value = (typeof orig === 'string' && orig.startsWith('{'))
|
|
244
|
+
? resolveRefs(orig) : buildShadow(orig, resolveRefs);
|
|
245
|
+
|
|
246
|
+
} else if (type === 'gradient' && options.outputReferences) {
|
|
247
|
+
value = (typeof orig === 'string' && orig.startsWith('{'))
|
|
248
|
+
? resolveRefs(orig) : buildGradient(orig, resolveRefs);
|
|
249
|
+
|
|
250
|
+
} else if ((type === 'border' || type === 'outline') && options.outputReferences) {
|
|
251
|
+
value = (typeof orig === 'string' && orig.startsWith('{'))
|
|
252
|
+
? resolveRefs(orig) : buildBorderLike(orig, resolveRefs);
|
|
253
|
+
|
|
254
|
+
} else if (type === 'transition' && options.outputReferences) {
|
|
255
|
+
value = (typeof orig === 'string' && orig.startsWith('{'))
|
|
256
|
+
? resolveRefs(orig) : buildTransition(orig, resolveRefs);
|
|
257
|
+
|
|
258
|
+
} else if (type === 'animation' && options.outputReferences) {
|
|
259
|
+
value = (typeof orig === 'string' && orig.startsWith('{'))
|
|
260
|
+
? resolveRefs(orig) : buildAnimation(orig, resolveRefs);
|
|
261
|
+
|
|
262
|
+
} else if (options.outputReferences && typeof orig === 'string' && orig.includes('{')) {
|
|
263
|
+
// Covers both plain aliases and CSS functions containing {references}
|
|
264
|
+
// e.g. color-mix(in srgb, {btn.secondary.background.color} 60%, #000)
|
|
265
|
+
// {references} are replaced with var(--name); arithmetic gets calc().
|
|
266
|
+
const resolved = resolveRefs(orig);
|
|
267
|
+
// Strip var(...) and CSS function calls (word chars + hyphens followed by '(')
|
|
268
|
+
// before checking for arithmetic operators, so that hyphens in names like
|
|
269
|
+
// "color-mix" or "linear-gradient" don't trigger a spurious calc() wrap.
|
|
270
|
+
const stripped = resolved
|
|
271
|
+
.replace(/var\([^)]+\)/g, '0')
|
|
272
|
+
.replace(/[\w-]+\(/g, '(');
|
|
273
|
+
const isCalcNeeded = /[+\-*/]/.test(stripped);
|
|
274
|
+
value = isCalcNeeded ? `calc(${resolved})` : resolved;
|
|
275
|
+
|
|
307
276
|
} else {
|
|
308
|
-
|
|
277
|
+
value = String(token.$value ?? token.value);
|
|
309
278
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
279
|
+
|
|
280
|
+
// Multi-line values (e.g. Open Props' linear() easings) are collapsed
|
|
281
|
+
// to a single line: declarations are parsed one line at a time (see
|
|
282
|
+
// parseCustomProps in ../merge-css.mjs), so a multi-line value would
|
|
283
|
+
// be truncated to its first line.
|
|
284
|
+
value = String(value)
|
|
285
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
286
|
+
.replace(/\(\s+/g, '(')
|
|
287
|
+
.replace(/\s+\)/g, ')');
|
|
288
|
+
|
|
289
|
+
return trackSource(token, [` --${token.name}: ${value};${commentSuffix}`]);
|
|
319
290
|
});
|
|
320
291
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
292
|
+
// Build a name → declaration-tail map from the declarations generated
|
|
293
|
+
// above, then merge with any pre-existing custom properties loaded via
|
|
294
|
+
// loadExistingCustomProps() / loadExistingCustomPropsScoped() (see
|
|
295
|
+
// ../merge-css.mjs). Pre-existing values (including trailing comments)
|
|
296
|
+
// take priority when both exist; pre-existing-only properties are kept.
|
|
297
|
+
// options.mode (sourceModes build only, see ../build-source-modes.mjs)
|
|
298
|
+
// scopes the merge to that mode, so same-named props with different
|
|
299
|
+
// values across modes are never mixed up.
|
|
300
|
+
const generatedProps = parseCustomProps(lines.join('\n'));
|
|
301
|
+
return mergeCustomProps(generatedProps, options.mode ?? null, sourceFileMap);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Builds the final CSS block text (`<selector> { ... }`) from an already
|
|
306
|
+
* computed name → declaration-tail map (see computeFinalProps() above):
|
|
307
|
+
* sorts properties alphabetically, pulls out customPropsGroups, prepends an
|
|
308
|
+
* optional `color-scheme` declaration, and optionally wraps the result in
|
|
309
|
+
* `@layer <addLayer> { ... }`.
|
|
310
|
+
*
|
|
311
|
+
* Extracted as its own function (rather than inline in the format() call
|
|
312
|
+
* below) so build-tokens-src/light-dark.mjs can re-run it on an adjusted
|
|
313
|
+
* props map — e.g. after moving properties shared by `light` and `dark` into
|
|
314
|
+
* a single `light-dark()` declaration — and get back a properly sorted and
|
|
315
|
+
* grouped block, instead of appending raw text after the fact.
|
|
316
|
+
* @param {object} opts
|
|
317
|
+
* @param {Record<string,string>} opts.finalProps name → declaration tail, as returned by computeFinalProps()
|
|
318
|
+
* @param {string} [opts.selector] CSS selector wrapping the block (default: (default: ':root'))
|
|
319
|
+
* @param {string} [opts.colorScheme] optional value for a `color-scheme: <value>;` declaration prepended to the block
|
|
320
|
+
* @param {{name:string,prefixes:string[]}[]} [opts.customPropsGroups] named groups of name prefixes, moved to the top of the block in list order (default: [])
|
|
321
|
+
* @param {string|null} [opts.addLayer] wraps the block in `@layer <addLayer> { ... }` (default: null — no layer)
|
|
322
|
+
* @returns {string}
|
|
323
|
+
* @example
|
|
324
|
+
* buildCssBlock({
|
|
325
|
+
* finalProps: { 'accent-color': '#123;' },
|
|
326
|
+
* selector: ':root', // default: ':root'
|
|
327
|
+
* colorScheme: 'light dark', // default: undefined — no color-scheme line
|
|
328
|
+
* customPropsGroups: [], // default: []
|
|
329
|
+
* addLayer: null, // default: null
|
|
330
|
+
* });
|
|
331
|
+
*/
|
|
332
|
+
export const buildCssBlock = ({
|
|
333
|
+
finalProps,
|
|
334
|
+
selector = ':root',
|
|
335
|
+
colorScheme,
|
|
336
|
+
customPropsGroups = [],
|
|
337
|
+
addLayer = null,
|
|
338
|
+
}) => {
|
|
339
|
+
// Final output is always sorted alphabetically ascending by property
|
|
340
|
+
// name, regardless of merge — this also keeps pre-existing-only
|
|
341
|
+
// properties from merge-css.mjs in order instead of trailing at the end.
|
|
342
|
+
const sortedProps = Object.entries(finalProps)
|
|
343
|
+
.sort(([a], [b]) => a.localeCompare(b, 'en', { numeric: true, sensitivity: 'base' }));
|
|
344
|
+
|
|
345
|
+
// customPropsGroups: properties whose first hyphen-separated name segment
|
|
346
|
+
// matches one of a group's prefixes are pulled out, labelled with that
|
|
347
|
+
// group's name and placed, in group-list order, at the beginning of the
|
|
348
|
+
// output file. A property matches the first group (in list order) whose
|
|
349
|
+
// prefixes include it.
|
|
350
|
+
const groupedProps = customPropsGroups.map(() => []);
|
|
351
|
+
const restProps = [];
|
|
352
|
+
for (const entry of sortedProps) {
|
|
353
|
+
const prefix = entry[0].split('-')[0];
|
|
354
|
+
const groupIndex = customPropsGroups.findIndex((g) => g.prefixes?.includes(prefix));
|
|
355
|
+
if (groupIndex !== -1) {
|
|
356
|
+
groupedProps[groupIndex].push(entry);
|
|
357
|
+
} else {
|
|
358
|
+
restProps.push(entry);
|
|
332
359
|
}
|
|
360
|
+
}
|
|
361
|
+
groupedProps.forEach((props, i) => {
|
|
362
|
+
const prefixes = customPropsGroups[i].prefixes;
|
|
363
|
+
props.sort(([a], [b]) => {
|
|
364
|
+
const idxA = prefixes.indexOf(a.split('-')[0]);
|
|
365
|
+
const idxB = prefixes.indexOf(b.split('-')[0]);
|
|
366
|
+
if (idxA !== idxB) return idxA - idxB;
|
|
367
|
+
return a.localeCompare(b, 'en', { numeric: true, sensitivity: 'base' });
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// A blank line is inserted between blocks of properties sharing the same
|
|
372
|
+
// first hyphen-separated segment (e.g. all `btn-*` together), to visually
|
|
373
|
+
// group related custom properties in the generated file.
|
|
374
|
+
const outLines = [];
|
|
333
375
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
376
|
+
if (colorScheme) {
|
|
377
|
+
outLines.push(` color-scheme: ${colorScheme};`, '');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let prevPrefix = null;
|
|
381
|
+
const appendBlock = (props) => {
|
|
382
|
+
for (const [name, tail] of props) {
|
|
383
|
+
const prefix = name.split('-')[0];
|
|
384
|
+
if (prevPrefix !== null && prefix !== prevPrefix) {
|
|
385
|
+
outLines.push('');
|
|
343
386
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const hasGroups = groupedProps.some((props) => props.length);
|
|
347
|
-
let isFirstBlock = true;
|
|
348
|
-
groupedProps.forEach((props, i) => {
|
|
349
|
-
if (!props.length) return;
|
|
350
|
-
if (!isFirstBlock) outLines.push('');
|
|
351
|
-
isFirstBlock = false;
|
|
352
|
-
outLines.push(buildGroupTitle(customPropsGroups[i].name));
|
|
353
|
-
appendBlock(props);
|
|
354
|
-
prevPrefix = null;
|
|
355
|
-
});
|
|
356
|
-
if (hasGroups && restProps.length) {
|
|
357
|
-
outLines.push('', ' /* ---------------------- */', '');
|
|
358
|
-
prevPrefix = null;
|
|
387
|
+
outLines.push(` --${name}: ${tail}`);
|
|
388
|
+
prevPrefix = prefix;
|
|
359
389
|
}
|
|
360
|
-
|
|
390
|
+
};
|
|
361
391
|
|
|
362
|
-
|
|
363
|
-
|
|
392
|
+
const hasGroups = groupedProps.some((props) => props.length);
|
|
393
|
+
let isFirstBlock = true;
|
|
394
|
+
groupedProps.forEach((props, i) => {
|
|
395
|
+
if (!props.length) return;
|
|
396
|
+
if (!isFirstBlock) outLines.push('');
|
|
397
|
+
isFirstBlock = false;
|
|
398
|
+
outLines.push(buildGroupTitle(customPropsGroups[i].name));
|
|
399
|
+
appendBlock(props);
|
|
400
|
+
prevPrefix = null;
|
|
401
|
+
});
|
|
402
|
+
if (hasGroups && restProps.length) {
|
|
403
|
+
outLines.push('', ' /* ---------------------- */', '');
|
|
404
|
+
prevPrefix = null;
|
|
405
|
+
}
|
|
406
|
+
appendBlock(restProps);
|
|
407
|
+
|
|
408
|
+
const block = `${selector} {\n${outLines.join('\n')}\n}\n`;
|
|
409
|
+
|
|
410
|
+
// addLayer: wraps the whole block inside `@layer <name> { ... }`.
|
|
411
|
+
// Indentation is left to stylelint's fix step (run right after the build).
|
|
412
|
+
return addLayer
|
|
413
|
+
? `@layer ${addLayer} {\n\n${block}}\n`
|
|
414
|
+
: block;
|
|
415
|
+
};
|
|
364
416
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
417
|
+
StyleDictionary.registerFormat({
|
|
418
|
+
name: 'css/variables-sorted',
|
|
419
|
+
format: ({ dictionary, options }) => {
|
|
420
|
+
const finalProps = computeFinalProps(dictionary, options);
|
|
421
|
+
lastFinalProps = finalProps;
|
|
422
|
+
customPropsCount = Object.keys(finalProps).length;
|
|
423
|
+
|
|
424
|
+
return buildCssBlock({
|
|
425
|
+
finalProps,
|
|
426
|
+
selector: options.selector,
|
|
427
|
+
colorScheme: options.colorScheme,
|
|
428
|
+
customPropsGroups: options.customPropsGroups,
|
|
429
|
+
addLayer: options.addLayer,
|
|
430
|
+
});
|
|
370
431
|
},
|
|
371
432
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// build-tokens-src/light-dark.mjs
|
|
2
|
+
// Supports the `useLightDarkFunc` config option (see config.mjs): with a
|
|
3
|
+
// sourceModes build (see build-source-modes.mjs) that defines both a `light`
|
|
4
|
+
// and a `dark` mode, custom properties present in BOTH — matched by name —
|
|
5
|
+
// are written once, as `--name: light-dark(<light-value>, <dark-value>);`,
|
|
6
|
+
// instead of being split across the base mode's top-level `:root { ... }`
|
|
7
|
+
// block and a `@media (prefers-color-scheme: dark) { ... }` block.
|
|
8
|
+
//
|
|
9
|
+
// Only `light` and `dark` are affected:
|
|
10
|
+
// - a third mode (e.g. a custom `high-contrast` key in sourceModes) is left
|
|
11
|
+
// completely untouched, still nested under its own
|
|
12
|
+
// `@media (prefers-color-scheme: <mode>) { ... }` rule
|
|
13
|
+
// - a property present in only one of `light`/`dark` is left in its own
|
|
14
|
+
// mode's block — still behind that mode's `@media` rule, unless that mode
|
|
15
|
+
// happens to be the configured base mode
|
|
16
|
+
//
|
|
17
|
+
// The shared `light-dark()` declarations always land in the configured base
|
|
18
|
+
// mode's block (build-source-modes.mjs' `sourceModesBase`, whichever of
|
|
19
|
+
// `light`, `dark`, or a third mode that is) — never specifically in `light`'s
|
|
20
|
+
// or `dark`'s own block — since the light-dark() function resolves against
|
|
21
|
+
// the effective `color-scheme`, regardless of whether the declaration itself
|
|
22
|
+
// sits inside a `@media (prefers-color-scheme: ...)` rule or not.
|
|
23
|
+
//
|
|
24
|
+
// Interaction with `mergeCustomProps`: nothing special is needed here — the
|
|
25
|
+
// per-mode merge with pre-existing custom properties (see ../merge-css.mjs)
|
|
26
|
+
// already runs earlier, inside computeFinalProps() (formats/css.mjs), before
|
|
27
|
+
// this module ever sees the props. So a pre-existing file written with the
|
|
28
|
+
// classic `@media (prefers-color-scheme: dark)` split (i.e. built before
|
|
29
|
+
// useLightDarkFunc was turned on) is reconciled into light-dark() calls just
|
|
30
|
+
// like freshly generated values, matched by property name. As the values
|
|
31
|
+
// merged in this way were never meant to be combined into light-dark() calls,
|
|
32
|
+
// unexpected pre-existing content (e.g. a manually-added property whose name
|
|
33
|
+
// happens to collide across modes) can produce an invalid combined value —
|
|
34
|
+
// left for the user to fix, same as any other mergeCustomProps edge case.
|
|
35
|
+
|
|
36
|
+
import { buildCssBlock } from './formats/css.mjs';
|
|
37
|
+
|
|
38
|
+
// Splits a declaration tail (as produced by computeFinalProps(): value +
|
|
39
|
+
// terminating `;` + optional trailing comment) into its bare CSS value and
|
|
40
|
+
// its trailing comment, so the value can be embedded as a light-dark()
|
|
41
|
+
// argument without dragging the `;`/comment along.
|
|
42
|
+
/** @param {string} tail @returns {{value: string, comment: string}} */
|
|
43
|
+
const splitTail = (tail) => {
|
|
44
|
+
const match = /^(.*?);\s*(\/\*.*\*\/)?\s*$/.exec(tail.trim());
|
|
45
|
+
if (!match) return { value: tail.trim().replace(/;$/, ''), comment: '' };
|
|
46
|
+
return { value: match[1].trim(), comment: match[2] ?? '' };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {object} opts
|
|
51
|
+
* @param {Record<string, Record<string,string>>} opts.modeFinalProps mode → (name → declaration tail) map, one entry per sourceModes key — as captured from computeFinalProps()/lastFinalProps (see formats/css.mjs) right after each per-mode sd.formatPlatform('css') call in build-source-modes.mjs
|
|
52
|
+
* @param {string[]} opts.modeNames all sourceModes keys, in config order
|
|
53
|
+
* @param {string} opts.baseMode mode whose block is printed as a top-level, unconditioned `:root` (or custom selector) — see sourceModesBase in config.mjs
|
|
54
|
+
* @param {string} [opts.selector] customPropsSelector — passed through unchanged to buildCssBlock() (default: ':root')
|
|
55
|
+
* @param {{name:string,prefixes:string[]}[]} [opts.customPropsGroups]
|
|
56
|
+
* @returns {{blocks: Record<string,string>, counts: Record<string,number>}|null}
|
|
57
|
+
* Rebuilt CSS block text + custom-prop count, keyed by mode, for every mode
|
|
58
|
+
* touched by the reconciliation (`light`, `dark`, and `baseMode`, which may
|
|
59
|
+
* coincide) — or null when sourceModes does not define both `light` and
|
|
60
|
+
* `dark` (useLightDarkFunc then has no effect, see config.mjs)
|
|
61
|
+
*/
|
|
62
|
+
export const applyLightDarkFunc = ({ modeFinalProps, modeNames, baseMode, selector, customPropsGroups }) => {
|
|
63
|
+
if (!modeNames.includes('light') || !modeNames.includes('dark')) return null;
|
|
64
|
+
|
|
65
|
+
const lightProps = modeFinalProps.light;
|
|
66
|
+
const darkProps = modeFinalProps.dark;
|
|
67
|
+
const commonNames = Object.keys(lightProps).filter((name) => name in darkProps);
|
|
68
|
+
|
|
69
|
+
/** @type {Record<string,string>} */
|
|
70
|
+
const sharedProps = {};
|
|
71
|
+
for (const name of commonNames) {
|
|
72
|
+
const light = splitTail(lightProps[name]);
|
|
73
|
+
const dark = splitTail(darkProps[name]);
|
|
74
|
+
// Prefers the light-side trailing comment (e.g. a token $description);
|
|
75
|
+
// falls back to the dark-side one if light has none.
|
|
76
|
+
const comment = light.comment || dark.comment;
|
|
77
|
+
sharedProps[name] = `light-dark(${light.value}, ${dark.value});${comment ? ` ${comment}` : ''}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stripCommon = (props) => {
|
|
81
|
+
const copy = { ...props };
|
|
82
|
+
for (const name of commonNames) delete copy[name];
|
|
83
|
+
return copy;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** @type {Record<string, Record<string,string>>} */
|
|
87
|
+
const updatedFinalProps = { ...modeFinalProps };
|
|
88
|
+
updatedFinalProps.light = stripCommon(lightProps);
|
|
89
|
+
updatedFinalProps.dark = stripCommon(darkProps);
|
|
90
|
+
// The shared declarations always land in the base mode's own map — adding
|
|
91
|
+
// them back in if the base mode happens to be `light` or `dark` itself
|
|
92
|
+
// (whose map was just stripped above), or into a third mode's map.
|
|
93
|
+
updatedFinalProps[baseMode] = { ...updatedFinalProps[baseMode], ...sharedProps };
|
|
94
|
+
|
|
95
|
+
// The base mode's `color-scheme` value is always normalised to read
|
|
96
|
+
// "light dark [...other modes]", with `light` before `dark`, regardless of
|
|
97
|
+
// the order sourceModes keys were declared in config or of which of the
|
|
98
|
+
// two is the configured base — light-dark() itself is positional
|
|
99
|
+
// (light-value first, dark-value second), so the accompanying
|
|
100
|
+
// `color-scheme` declaration follows the same, predictable reading. Modes
|
|
101
|
+
// other than light/dark, if any, are appended after, unaffected.
|
|
102
|
+
const restModes = modeNames.filter((mode) => mode !== 'light' && mode !== 'dark');
|
|
103
|
+
const colorScheme = ['light', 'dark', ...restModes].join(' ');
|
|
104
|
+
|
|
105
|
+
// Only `light`, `dark`, and the base mode (which may be one of the two, or
|
|
106
|
+
// a third mode) need re-rendering: every other sourceModes key is
|
|
107
|
+
// untouched and keeps its already-built block (see build-source-modes.mjs).
|
|
108
|
+
const modesToRebuild = new Set(['light', 'dark', baseMode]);
|
|
109
|
+
|
|
110
|
+
/** @type {Record<string,string>} */
|
|
111
|
+
const blocks = {};
|
|
112
|
+
/** @type {Record<string,number>} */
|
|
113
|
+
const counts = {};
|
|
114
|
+
for (const mode of modesToRebuild) {
|
|
115
|
+
const finalProps = updatedFinalProps[mode];
|
|
116
|
+
blocks[mode] = buildCssBlock({
|
|
117
|
+
finalProps,
|
|
118
|
+
selector,
|
|
119
|
+
colorScheme: mode === baseMode ? colorScheme : mode,
|
|
120
|
+
customPropsGroups,
|
|
121
|
+
});
|
|
122
|
+
counts[mode] = Object.keys(finalProps).length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { blocks, counts };
|
|
126
|
+
};
|