@massimo-cassandro/minimo 1.37.0 → 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/build-tokens-src/build-source-modes.mjs +57 -14
- package/dev-tools/design-tokens-builder/build-tokens-src/config.mjs +20 -0
- 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 +3 -0
- package/dev-tools/design-tokens-builder/build-tokens.mjs +5 -0
- package/dev-tools/design-tokens-builder/tokens-config-sample.mjs +45 -3
- package/dev-tools/starter-kit/starter-install.sh +1 -1
- package/package.json +1 -1
|
@@ -4,18 +4,25 @@
|
|
|
4
4
|
// light/dark custom-properties split.
|
|
5
5
|
//
|
|
6
6
|
// For each mode, a separate StyleDictionary instance is built from that
|
|
7
|
-
// mode's own `source` array. The generated
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// (e.g.
|
|
7
|
+
// mode's own `source` array. The generated `<selector> { ... }` CSS blocks
|
|
8
|
+
// (selector: customPropsSelector, default ':root' — see config.mjs) are then
|
|
9
|
+
// composed into a single destFile:
|
|
10
|
+
// - the base mode's block stays as a plain `<selector> { ... }` rule, with
|
|
11
|
+
// a `color-scheme: <all modes joined>;` declaration prepended (e.g.
|
|
12
|
+
// "light dark")
|
|
12
13
|
// - every other mode's block is wrapped in
|
|
13
|
-
// `@media (prefers-color-scheme: <mode>) {
|
|
14
|
+
// `@media (prefers-color-scheme: <mode>) { <selector> { ... } }`, with a
|
|
14
15
|
// `color-scheme: <mode>;` declaration prepended
|
|
15
16
|
// customPropsGroups, mergeCustomProps and pxToRem apply per mode, exactly as
|
|
16
17
|
// in the single-source build. addLayer (if set) wraps the whole composed
|
|
17
18
|
// result in a single `@layer`, rather than one per mode.
|
|
18
19
|
//
|
|
20
|
+
// When sourceModes defines both a `light` and a `dark` key and
|
|
21
|
+
// useLightDarkFunc is true (default), custom properties shared by both are
|
|
22
|
+
// reconciled into a single `light-dark()` declaration instead of being split
|
|
23
|
+
// across the base block and a `@media (prefers-color-scheme: dark)` rule —
|
|
24
|
+
// see light-dark.mjs.
|
|
25
|
+
//
|
|
19
26
|
// JSON tokens (if jsonBuildPath is set) are produced per mode: one set of
|
|
20
27
|
// files per mode, with `-<mode>` appended to each filename (see
|
|
21
28
|
// buildJsonFiles() in formats/json.mjs), e.g. "tokens-light.jsonc" /
|
|
@@ -26,15 +33,16 @@ import StyleDictionary from 'style-dictionary';
|
|
|
26
33
|
import * as path from 'node:path';
|
|
27
34
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
28
35
|
import { CSS_TRANSFORMS, JSON_TRANSFORMS } from './platforms.mjs';
|
|
29
|
-
import { customPropsCount } from './formats/css.mjs';
|
|
36
|
+
import { customPropsCount, lastFinalProps } from './formats/css.mjs';
|
|
30
37
|
import { buildJsonFiles, collectConcreteFilePaths } from './formats/json.mjs';
|
|
31
38
|
import { loadExistingCustomPropsScoped } from './merge-css.mjs';
|
|
32
39
|
import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
|
|
40
|
+
import { applyLightDarkFunc } from './light-dark.mjs';
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
43
|
* @param {object} opts
|
|
36
44
|
* @param {Record<string,string[]>} opts.sourceModes Resolved source paths per mode (see config.mjs)
|
|
37
|
-
* @param {string} opts.baseMode Mode whose block stays a top-level
|
|
45
|
+
* @param {string} opts.baseMode Mode whose block stays a top-level `<selector> { ... }` rule
|
|
38
46
|
* @param {string} opts.buildPath Absolute path for CSS output directory
|
|
39
47
|
* @param {string} opts.destFile CSS output filename
|
|
40
48
|
* @param {string|null} opts.jsonBuildPath Absolute path for JSON output (null = disabled)
|
|
@@ -45,6 +53,8 @@ import { LEGACY_TOKENS_PARSER_NAME } from './legacy-tokens-parser.mjs';
|
|
|
45
53
|
* @param {boolean} opts.pxToRem
|
|
46
54
|
* @param {string|null} opts.addLayer
|
|
47
55
|
* @param {boolean|(string|RegExp)[]} opts.mergeCustomProps true = merge all sources, array = only the matching token source files
|
|
56
|
+
* @param {boolean} opts.useLightDarkFunc reconcile shared `light`/`dark` properties into `light-dark()` calls (default: true — see config.mjs and light-dark.mjs). Ignored unless sourceModes defines both a `light` and a `dark` key.
|
|
57
|
+
* @param {string} opts.customPropsSelector CSS selector wrapping the generated block(s) (default: ':root' — see config.mjs)
|
|
48
58
|
* @returns {Promise<{cssDestPath: string, totalCustomProps: number, jsonFilesByMode: Record<string, object[]>}>}
|
|
49
59
|
*/
|
|
50
60
|
export const buildSourceModes = async ({
|
|
@@ -60,13 +70,15 @@ export const buildSourceModes = async ({
|
|
|
60
70
|
pxToRem,
|
|
61
71
|
addLayer,
|
|
62
72
|
mergeCustomProps,
|
|
73
|
+
useLightDarkFunc,
|
|
74
|
+
customPropsSelector,
|
|
63
75
|
}) => {
|
|
64
76
|
const modeNames = Object.keys(sourceModes);
|
|
65
77
|
|
|
66
78
|
// Read the pre-existing destFile (if any) BEFORE any Style Dictionary
|
|
67
79
|
// instance runs, since the first one to write would overwrite it.
|
|
68
80
|
if (mergeCustomProps) {
|
|
69
|
-
loadExistingCustomPropsScoped(path.join(buildPath, destFile), baseMode, mergeCustomProps);
|
|
81
|
+
loadExistingCustomPropsScoped(path.join(buildPath, destFile), baseMode, mergeCustomProps, customPropsSelector);
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
const cssTransforms = pxToRem
|
|
@@ -75,13 +87,22 @@ export const buildSourceModes = async ({
|
|
|
75
87
|
|
|
76
88
|
/** @type {Record<string,string>} */
|
|
77
89
|
const modeBlocks = {};
|
|
78
|
-
|
|
90
|
+
// Per-mode custom-prop count and structured (name -> tail) props map,
|
|
91
|
+
// captured right after each per-mode format call (customPropsCount and
|
|
92
|
+
// lastFinalProps are live bindings, updated synchronously by the format
|
|
93
|
+
// function — see formats/css.mjs). Kept per mode (rather than summed on
|
|
94
|
+
// the fly) so applyLightDarkFunc() can later overwrite just the entries it
|
|
95
|
+
// actually touches (light, dark, and the base mode).
|
|
96
|
+
/** @type {Record<string,number>} */
|
|
97
|
+
const modeCounts = {};
|
|
98
|
+
/** @type {Record<string, Record<string,string>>} */
|
|
99
|
+
const modeFinalProps = {};
|
|
79
100
|
/** @type {Record<string, object[]>} */
|
|
80
101
|
const jsonFilesByMode = {};
|
|
81
102
|
|
|
82
103
|
for (const mode of modeNames) {
|
|
83
104
|
// Non-base modes may reference base-mode tokens (always defined, since
|
|
84
|
-
// the base block is the unconditional top-level
|
|
105
|
+
// the base block is the unconditional top-level `<selector>`). The base
|
|
85
106
|
// sources are passed as `include`: available for reference resolution
|
|
86
107
|
// but flagged isSource: false, and skipped by the css/json formats.
|
|
87
108
|
// Base-mode-only on purpose: a reference to a token that exists only in
|
|
@@ -102,6 +123,7 @@ export const buildSourceModes = async ({
|
|
|
102
123
|
format: 'css/variables-sorted',
|
|
103
124
|
options: {
|
|
104
125
|
outputReferences: true,
|
|
126
|
+
selector: customPropsSelector,
|
|
105
127
|
customPropsGroups,
|
|
106
128
|
mode,
|
|
107
129
|
colorScheme: mode === baseMode ? modeNames.join(' ') : mode,
|
|
@@ -116,9 +138,10 @@ export const buildSourceModes = async ({
|
|
|
116
138
|
// the composed multi-mode file is written once, at the end, below.
|
|
117
139
|
const [formatted] = await sd.formatPlatform('css');
|
|
118
140
|
modeBlocks[mode] = /** @type {string} */ (formatted.output);
|
|
119
|
-
// customPropsCount
|
|
120
|
-
// format function called above (see formats/css.mjs).
|
|
121
|
-
|
|
141
|
+
// customPropsCount / lastFinalProps are live bindings, updated
|
|
142
|
+
// synchronously by the format function called above (see formats/css.mjs).
|
|
143
|
+
modeCounts[mode] = customPropsCount;
|
|
144
|
+
modeFinalProps[mode] = lastFinalProps;
|
|
122
145
|
|
|
123
146
|
if (jsonBuildPath) {
|
|
124
147
|
const concreteFilePaths = jsonDestFile ? [] : await collectConcreteFilePaths(sd);
|
|
@@ -142,6 +165,26 @@ export const buildSourceModes = async ({
|
|
|
142
165
|
}
|
|
143
166
|
}
|
|
144
167
|
|
|
168
|
+
// ── light-dark() reconciliation (light/dark modes only) ──────────────────
|
|
169
|
+
// Rebuilds just the `light`, `dark` and base-mode blocks from the adjusted
|
|
170
|
+
// props maps — see light-dark.mjs. No-op (returns null) unless sourceModes
|
|
171
|
+
// defines both a `light` and a `dark` key.
|
|
172
|
+
if (useLightDarkFunc) {
|
|
173
|
+
const reconciled = applyLightDarkFunc({
|
|
174
|
+
modeFinalProps,
|
|
175
|
+
modeNames,
|
|
176
|
+
baseMode,
|
|
177
|
+
selector: customPropsSelector,
|
|
178
|
+
customPropsGroups,
|
|
179
|
+
});
|
|
180
|
+
if (reconciled) {
|
|
181
|
+
Object.assign(modeBlocks, reconciled.blocks);
|
|
182
|
+
Object.assign(modeCounts, reconciled.counts);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const totalCustomProps = Object.values(modeCounts).reduce((sum, n) => sum + n, 0);
|
|
187
|
+
|
|
145
188
|
// ── Compose the final CSS ────────────────────────────────────────────────
|
|
146
189
|
const otherModes = modeNames.filter((mode) => mode !== baseMode);
|
|
147
190
|
|
|
@@ -103,6 +103,26 @@ export const mergeCustomProps = (() => {
|
|
|
103
103
|
// Set to null, undefined or false (default) to emit no layer.
|
|
104
104
|
export const addLayer = buildConfig.addLayer ? String(buildConfig.addLayer).trim() : null;
|
|
105
105
|
|
|
106
|
+
// customPropsSelector: CSS selector wrapping the generated custom properties
|
|
107
|
+
// block(s) (see formats/css.mjs buildCssBlock()), instead of the default
|
|
108
|
+
// `:root`. Applies uniformly to a single-source build and to a sourceModes
|
|
109
|
+
// build (the base mode's top-level block and every other mode's block
|
|
110
|
+
// nested under its own `@media (prefers-color-scheme: <mode>)` rule all use
|
|
111
|
+
// the same selector). Falsy or blank values fall back to ':root'.
|
|
112
|
+
export const customPropsSelector = typeof buildConfig.customPropsSelector === 'string'
|
|
113
|
+
&& buildConfig.customPropsSelector.trim()
|
|
114
|
+
? buildConfig.customPropsSelector.trim()
|
|
115
|
+
: ':root';
|
|
116
|
+
|
|
117
|
+
// useLightDarkFunc: with sourceModes set (see below) and defining both a
|
|
118
|
+
// `light` and a `dark` key, custom properties present in both are written
|
|
119
|
+
// once as `--name: light-dark(<light>, <dark>);` instead of being split
|
|
120
|
+
// across the base mode's block and a
|
|
121
|
+
// `@media (prefers-color-scheme: dark) { ... }` rule (see light-dark.mjs).
|
|
122
|
+
// Ignored when sourceModes is not set, or does not define both a light and a
|
|
123
|
+
// dark mode. Default: true.
|
|
124
|
+
export const useLightDarkFunc = buildConfig.useLightDarkFunc !== false;
|
|
125
|
+
|
|
106
126
|
// customPropsGroups: custom properties whose first hyphen-separated name
|
|
107
127
|
// segment matches one of a group's prefixes are pulled out, labelled with
|
|
108
128
|
// that group's name and moved to the beginning of the generated CSS file, in
|
|
@@ -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
|
+
};
|
|
@@ -67,6 +67,12 @@ const matchBraces = (text, openIndex) => {
|
|
|
67
67
|
return -1;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
// Escapes a string for literal use inside a RegExp — needed because
|
|
71
|
+
// customPropsSelector (see config.mjs) can contain regex-special characters,
|
|
72
|
+
// e.g. ':where(html)'.
|
|
73
|
+
/** @param {string} str */
|
|
74
|
+
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
75
|
+
|
|
70
76
|
// Finds every @media (prefers-color-scheme: <mode>) { ... } block in a
|
|
71
77
|
// previously generated sourceModes CSS destFile, with absolute character
|
|
72
78
|
// offsets into cssText. Used to split the file into per-mode scopes (see
|
|
@@ -93,22 +99,25 @@ export const findMediaModeBlocks = (cssText) => {
|
|
|
93
99
|
|
|
94
100
|
// Splits a previously generated sourceModes CSS destFile into per-mode
|
|
95
101
|
// scopes (see build-source-modes.mjs for the shape of the generated file):
|
|
96
|
-
// - the base scope is the top-level
|
|
97
|
-
// NOT nested inside a `@media (prefers-color-scheme: ...)` rule
|
|
98
|
-
// - every other scope is the
|
|
99
|
-
// `@media (prefers-color-scheme: <mode>) { ... }` rule
|
|
102
|
+
// - the base scope is the top-level `<selector> { ... }` block, i.e. the
|
|
103
|
+
// one NOT nested inside a `@media (prefers-color-scheme: ...)` rule
|
|
104
|
+
// - every other scope is the `<selector> { ... }` block nested inside its
|
|
105
|
+
// own `@media (prefers-color-scheme: <mode>) { ... }` rule
|
|
100
106
|
// @param {string} cssText
|
|
107
|
+
// @param {string} [selector] the customPropsSelector the file was generated
|
|
108
|
+
// with (default: ':root' — see config.mjs)
|
|
101
109
|
// @returns {Record<string,string>} mode → CSS body text (base mode keyed as '__base__')
|
|
102
|
-
const splitModeScopes = (cssText) => {
|
|
110
|
+
const splitModeScopes = (cssText, selector = ':root') => {
|
|
103
111
|
/** @type {Record<string,string>} */
|
|
104
112
|
const scopes = {};
|
|
113
|
+
const selectorRe = new RegExp(`${escapeRegExp(selector)}\\s*\\{`);
|
|
105
114
|
|
|
106
115
|
// 1. Every @media (prefers-color-scheme: <mode>) { ... } block, and the
|
|
107
|
-
//
|
|
116
|
+
// <selector> { ... } body nested inside each one.
|
|
108
117
|
const mediaBlocks = findMediaModeBlocks(cssText);
|
|
109
118
|
for (const { mode, start, end } of mediaBlocks) {
|
|
110
119
|
const body = cssText.slice(start + 1, end);
|
|
111
|
-
const rootMatch =
|
|
120
|
+
const rootMatch = selectorRe.exec(body);
|
|
112
121
|
if (rootMatch) {
|
|
113
122
|
const rootOpen = rootMatch.index + rootMatch[0].length - 1;
|
|
114
123
|
const rootClose = matchBraces(body, rootOpen);
|
|
@@ -118,9 +127,9 @@ const splitModeScopes = (cssText) => {
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
// 2. The base
|
|
122
|
-
// the media blocks found above.
|
|
123
|
-
const ROOT_RE =
|
|
130
|
+
// 2. The base <selector> { ... } block: the first one not nested inside
|
|
131
|
+
// any of the media blocks found above.
|
|
132
|
+
const ROOT_RE = new RegExp(`${escapeRegExp(selector)}\\s*\\{`, 'g');
|
|
124
133
|
for (const match of cssText.matchAll(ROOT_RE)) {
|
|
125
134
|
const openIdx = match.index + match[0].length - 1;
|
|
126
135
|
const insideMedia = mediaBlocks.some((b) => openIdx > b.start && openIdx < b.end);
|
|
@@ -188,22 +197,24 @@ export const loadExistingCustomProps = (absDestPath, sources = true) => {
|
|
|
188
197
|
/**
|
|
189
198
|
* sourceModes equivalent of loadExistingCustomProps(): reads the CSS
|
|
190
199
|
* destination file (if it exists) and stores a per-mode name → value map,
|
|
191
|
-
* scoped to each mode's own
|
|
200
|
+
* scoped to each mode's own `<selector> { ... }` block (see splitModeScopes()).
|
|
192
201
|
* @param {string} absDestPath absolute path of the CSS file to read
|
|
193
202
|
* @param {string} baseMode mode whose declarations live in the top-level,
|
|
194
|
-
* non-nested
|
|
203
|
+
* non-nested `<selector> { ... }` block (no default — always passed explicitly
|
|
195
204
|
* by build-source-modes.mjs, from the resolved sourceModesBase)
|
|
196
205
|
* @param {true|(string|RegExp)[]} [sources] `true` merges every source, an
|
|
197
206
|
* array only the token source files matching one of its entries (default: true)
|
|
207
|
+
* @param {string} [selector] the customPropsSelector the pre-existing file was
|
|
208
|
+
* generated with (default: ':root' — see config.mjs)
|
|
198
209
|
* @returns {Record<string, Record<string,string>>|null}
|
|
199
210
|
*/
|
|
200
|
-
export const loadExistingCustomPropsScoped = (absDestPath, baseMode, sources = true) => {
|
|
211
|
+
export const loadExistingCustomPropsScoped = (absDestPath, baseMode, sources = true, selector = ':root') => {
|
|
201
212
|
mergeSources = sources;
|
|
202
213
|
if (!existsSync(absDestPath)) {
|
|
203
214
|
scopedExistingCustomProps = null;
|
|
204
215
|
return scopedExistingCustomProps;
|
|
205
216
|
}
|
|
206
|
-
const scopes = splitModeScopes(readFileSync(absDestPath, 'utf8'));
|
|
217
|
+
const scopes = splitModeScopes(readFileSync(absDestPath, 'utf8'), selector);
|
|
207
218
|
scopedExistingCustomProps = {};
|
|
208
219
|
if (scopes.__base__ !== undefined) {
|
|
209
220
|
scopedExistingCustomProps[baseMode] = parseCustomProps(scopes.__base__);
|
|
@@ -52,6 +52,7 @@ export const JSON_TRANSFORMS = [
|
|
|
52
52
|
* @param {{name: string, prefixes: string[]}[]} opts.customPropsGroups Named groups of name prefixes, moved to the top of the CSS output in list order.
|
|
53
53
|
* @param {boolean} opts.pxToRem If false, skips the px→rem transform on the CSS platform. Default: true.
|
|
54
54
|
* @param {string|null} opts.addLayer Wraps the generated custom properties inside `@layer <addLayer> { ... }`. null = no layer.
|
|
55
|
+
* @param {string} opts.selector CSS selector wrapping the generated block (default: ':root').
|
|
55
56
|
* @returns {Record<string, PlatformConfig>} platforms object ready for Style Dictionary config
|
|
56
57
|
*/
|
|
57
58
|
export const buildPlatforms = ({
|
|
@@ -65,6 +66,7 @@ export const buildPlatforms = ({
|
|
|
65
66
|
customPropsGroups = [],
|
|
66
67
|
pxToRem = true,
|
|
67
68
|
addLayer = null,
|
|
69
|
+
selector = ':root',
|
|
68
70
|
}) => {
|
|
69
71
|
const cssTransforms = pxToRem
|
|
70
72
|
? CSS_TRANSFORMS
|
|
@@ -82,6 +84,7 @@ export const buildPlatforms = ({
|
|
|
82
84
|
options: {
|
|
83
85
|
outputReferences: true,
|
|
84
86
|
showFileHeader: true,
|
|
87
|
+
selector,
|
|
85
88
|
customPropsGroups,
|
|
86
89
|
addLayer,
|
|
87
90
|
},
|
|
@@ -41,6 +41,8 @@ import {
|
|
|
41
41
|
customPropsGroups,
|
|
42
42
|
pxToRem,
|
|
43
43
|
addLayer,
|
|
44
|
+
useLightDarkFunc,
|
|
45
|
+
customPropsSelector,
|
|
44
46
|
} from './build-tokens-src/config.mjs';
|
|
45
47
|
|
|
46
48
|
// ── 2. Custom transforms ─────────────────────────────────────────────────────
|
|
@@ -114,6 +116,8 @@ if (sourceModes) {
|
|
|
114
116
|
pxToRem,
|
|
115
117
|
addLayer,
|
|
116
118
|
mergeCustomProps,
|
|
119
|
+
useLightDarkFunc,
|
|
120
|
+
customPropsSelector,
|
|
117
121
|
});
|
|
118
122
|
|
|
119
123
|
} else {
|
|
@@ -166,6 +170,7 @@ if (sourceModes) {
|
|
|
166
170
|
customPropsGroups,
|
|
167
171
|
pxToRem,
|
|
168
172
|
addLayer,
|
|
173
|
+
selector: customPropsSelector,
|
|
169
174
|
}),
|
|
170
175
|
});
|
|
171
176
|
|
|
@@ -68,7 +68,7 @@ const config = {
|
|
|
68
68
|
// { src: `${node_modules_path}/open-props/open-props.style-dictionary-tokens.json`, prefix: 'op' },
|
|
69
69
|
|
|
70
70
|
// your project tokens
|
|
71
|
-
'./project-tokens/*.{js,mjs,jsonc,json}',
|
|
71
|
+
{src: './project-tokens/*.{js,mjs,jsonc,json}', prefix: 'my-project'}
|
|
72
72
|
],
|
|
73
73
|
|
|
74
74
|
// Optional light/dark (or other) custom properties split — alternative to
|
|
@@ -110,12 +110,54 @@ const config = {
|
|
|
110
110
|
// },
|
|
111
111
|
sourceModes: null,
|
|
112
112
|
|
|
113
|
+
// With sourceModes set, and only if it defines both a `light` and a `dark`
|
|
114
|
+
// key: custom properties present in BOTH — matched by name — are written
|
|
115
|
+
// once as `--name: light-dark(<light-value>, <dark-value>);`, instead of
|
|
116
|
+
// being split across the base mode's top-level block and a
|
|
117
|
+
// `@media (prefers-color-scheme: dark) { ... }` rule.
|
|
118
|
+
//
|
|
119
|
+
// Unaffected:
|
|
120
|
+
// - any other key in sourceModes besides light/dark (e.g. a third,
|
|
121
|
+
// custom mode) — always stays on its own
|
|
122
|
+
// `@media (prefers-color-scheme: <mode>) { ... }` rule
|
|
123
|
+
// - a property present in only one of `light`/`dark` — stays in its own
|
|
124
|
+
// mode's block (still behind `@media`, unless that mode happens to be
|
|
125
|
+
// sourceModesBase)
|
|
126
|
+
//
|
|
127
|
+
// Whenever this option is active (sourceModes has both a light and a dark
|
|
128
|
+
// key), the base mode's `color-scheme` declaration is always normalised to
|
|
129
|
+
// read "light dark [...any other mode]", with light before dark — matching
|
|
130
|
+
// light-dark()'s own (light-value, dark-value) argument order — regardless
|
|
131
|
+
// of the order sourceModes keys were declared in, or of which of the two
|
|
132
|
+
// is configured as sourceModesBase below.
|
|
133
|
+
//
|
|
134
|
+
// Interaction with mergeCustomProps: no special handling needed — the
|
|
135
|
+
// per-mode merge with pre-existing custom properties always runs first;
|
|
136
|
+
// this option then reconciles whatever value ends up in each mode
|
|
137
|
+
// (freshly generated or preserved from a pre-existing file) into
|
|
138
|
+
// light-dark(), matched by name. This also means that re-running a build
|
|
139
|
+
// with this option newly turned on, over a destFile previously generated
|
|
140
|
+
// with it off (classic `@media (prefers-color-scheme: dark)` split) and
|
|
141
|
+
// mergeCustomProps: true, converts the preserved values into light-dark()
|
|
142
|
+
// calls too — as with any mergeCustomProps case, a resulting mismatch is
|
|
143
|
+
// left for you to fix.
|
|
144
|
+
//
|
|
145
|
+
// Default: true. Ignored when sourceModes is not set (null/undefined).
|
|
146
|
+
useLightDarkFunc: true,
|
|
147
|
+
|
|
113
148
|
// With sourceModes set: the mode whose declarations go in the top-level
|
|
114
149
|
// `:root { ... }` block, instead of nested inside its own
|
|
115
150
|
// `@media (prefers-color-scheme: <mode>) { ... }` rule (default: the first
|
|
116
151
|
// key of sourceModes, e.g. 'light'). Ignored when sourceModes is not set.
|
|
117
152
|
sourceModesBase: 'light',
|
|
118
153
|
|
|
154
|
+
// CSS selector wrapping the generated custom properties block(s), instead
|
|
155
|
+
// of the default `:root`. Applies uniformly whether using a single
|
|
156
|
+
// `source` or `sourceModes` (the base mode's top-level block and every
|
|
157
|
+
// other mode's block nested under its own
|
|
158
|
+
// `@media (prefers-color-scheme: <mode>)` rule all use this same
|
|
159
|
+
// selector). Falsy or blank values fall back to ':root'.
|
|
160
|
+
customPropsSelector: ':where(html)',
|
|
119
161
|
|
|
120
162
|
// If true (default), dimension token values expressed in px are converted
|
|
121
163
|
// to rem in the generated CSS. Values in other units (em, %, vh, dvw, ...)
|
|
@@ -141,7 +183,7 @@ const config = {
|
|
|
141
183
|
// Comments at the end of a custom property line are preserved; full-line
|
|
142
184
|
// comments are lost, and a multi-line comment is cut at the end of its first
|
|
143
185
|
// line (and closed, to keep the CSS valid).
|
|
144
|
-
mergeCustomProps:
|
|
186
|
+
mergeCustomProps: true,
|
|
145
187
|
|
|
146
188
|
// Wraps the generated custom properties inside a CSS `@layer` at-rule,
|
|
147
189
|
// e.g. addLayer: 'project' -> `@layer project { :root { ... } }`.
|
|
@@ -155,7 +197,7 @@ const config = {
|
|
|
155
197
|
customPropsGroups: [
|
|
156
198
|
{
|
|
157
199
|
name: 'COLORS',
|
|
158
|
-
prefixes: ['accent', 'primary', 'secondary', 'neutral'],
|
|
200
|
+
prefixes: ['accent', 'primary', 'secondary', 'neutral', 'color'],
|
|
159
201
|
},
|
|
160
202
|
{
|
|
161
203
|
name: 'LAYOUT',
|
|
@@ -415,7 +415,7 @@ done
|
|
|
415
415
|
# installato in node_modules (stessa versione che userà il progetto)
|
|
416
416
|
MINIMO_PKG_DIR=./node_modules/@massimo-cassandro/minimo
|
|
417
417
|
|
|
418
|
-
force_cat "${MINIMO_PKG_DIR}/src/custom-properties.css" "${FRONTEND_INSTALL_PATH}/css/custom-properties.css"
|
|
418
|
+
force_cat "${MINIMO_PKG_DIR}/src/custom-properties.css" "${FRONTEND_INSTALL_PATH}/css/minimo-custom-properties.css"
|
|
419
419
|
force_cat "${MINIMO_PKG_DIR}/src/custom-media.css" "${FRONTEND_INSTALL_PATH}/css/custom-media.css"
|
|
420
420
|
|
|
421
421
|
# entry css principale del progetto: copia diretta di minimo.css, rinominato
|