postcss-ruler 1.3.0 → 2.0.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/README.md +164 -11
- package/index.js +63 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,6 +33,16 @@ module.exports = {
|
|
|
33
33
|
minWidth: 320, // Default minimum viewport width
|
|
34
34
|
maxWidth: 1760, // Default maximum viewport width
|
|
35
35
|
generateAllCrossPairs: false,
|
|
36
|
+
// Optional: Pre-define scales for cross-file usage (Astro, Vite, etc.)
|
|
37
|
+
scales: {
|
|
38
|
+
space: {
|
|
39
|
+
pairs: {
|
|
40
|
+
xs: [8, 16],
|
|
41
|
+
sm: [16, 24],
|
|
42
|
+
md: [24, 32],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
36
46
|
},
|
|
37
47
|
},
|
|
38
48
|
};
|
|
@@ -230,15 +240,157 @@ Convert individual values directly to `clamp()` functions:
|
|
|
230
240
|
}
|
|
231
241
|
```
|
|
232
242
|
|
|
243
|
+
### 4. Low Specificity Mode: Zero-Specificity Utilities
|
|
244
|
+
|
|
245
|
+
Wrap generated selectors in `:where()` to reduce their specificity to 0, making them easier to override:
|
|
246
|
+
|
|
247
|
+
```css
|
|
248
|
+
@ruler scale({
|
|
249
|
+
prefix: 'space',
|
|
250
|
+
pairs: {
|
|
251
|
+
"xs": [8, 16],
|
|
252
|
+
"sm": [16, 24]
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/* Enable lowSpecificity per utility */
|
|
257
|
+
@ruler utility({
|
|
258
|
+
selector: '.gap',
|
|
259
|
+
property: 'gap',
|
|
260
|
+
scale: 'space',
|
|
261
|
+
lowSpecificity: true
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Generates:**
|
|
266
|
+
|
|
267
|
+
```css
|
|
268
|
+
--space-xs: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem);
|
|
269
|
+
--space-sm: clamp(1rem, 0.5556vw + 0.8889rem, 1.5rem);
|
|
270
|
+
|
|
271
|
+
:where(.gap-xs) {
|
|
272
|
+
gap: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem);
|
|
273
|
+
}
|
|
274
|
+
:where(.gap-sm) {
|
|
275
|
+
gap: clamp(1rem, 0.5556vw + 0.8889rem, 1.5rem);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Specificity comparison:**
|
|
280
|
+
|
|
281
|
+
- `.gap-xs` → specificity (0,1,0)
|
|
282
|
+
- `:where(.gap-xs)` → specificity (0,0,0)
|
|
283
|
+
|
|
284
|
+
**Use case:** Design system utilities that should be easily overridable without `!important`:
|
|
285
|
+
|
|
286
|
+
```css
|
|
287
|
+
/* Utility with zero specificity */
|
|
288
|
+
:where(.gap-md) {
|
|
289
|
+
gap: clamp(1.5rem, 0.5556vw + 1.3889rem, 2rem);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Easy to override with a simple class */
|
|
293
|
+
.custom-layout {
|
|
294
|
+
gap: 2rem; /* This wins without !important */
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Works with attribute mode:**
|
|
299
|
+
|
|
300
|
+
```css
|
|
301
|
+
@ruler utility({
|
|
302
|
+
attribute: 'data-size',
|
|
303
|
+
property: 'font-size',
|
|
304
|
+
scale: 'size',
|
|
305
|
+
lowSpecificity: true
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Generates:**
|
|
310
|
+
|
|
311
|
+
```css
|
|
312
|
+
:where([data-size="xs"]) {
|
|
313
|
+
font-size: var(--size-xs);
|
|
314
|
+
}
|
|
315
|
+
:where([data-size="sm"]) {
|
|
316
|
+
font-size: var(--size-sm);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Global configuration:**
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
// postcss.config.js
|
|
324
|
+
module.exports = {
|
|
325
|
+
plugins: {
|
|
326
|
+
"postcss-ruler": {
|
|
327
|
+
lowSpecificity: true, // All utilities use :where() by default
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
You can override the global setting per utility by explicitly setting `lowSpecificity: false`.
|
|
334
|
+
|
|
233
335
|
## Configuration Options
|
|
234
336
|
|
|
235
337
|
### Plugin Options
|
|
236
338
|
|
|
237
|
-
| Option | Type | Default | Description
|
|
238
|
-
| ----------------------- | ------- | ------- |
|
|
239
|
-
| `minWidth` | number | `320` | Default minimum viewport width in pixels
|
|
240
|
-
| `maxWidth` | number | `1760` | Default maximum viewport width in pixels
|
|
241
|
-
| `generateAllCrossPairs` | boolean | `false` | Generate cross-combinations in scale mode
|
|
339
|
+
| Option | Type | Default | Description |
|
|
340
|
+
| ----------------------- | ------- | ------- | -------------------------------------------------------------- |
|
|
341
|
+
| `minWidth` | number | `320` | Default minimum viewport width in pixels |
|
|
342
|
+
| `maxWidth` | number | `1760` | Default maximum viewport width in pixels |
|
|
343
|
+
| `generateAllCrossPairs` | boolean | `false` | Generate cross-combinations in scale mode |
|
|
344
|
+
| `lowSpecificity` | boolean | `false` | Wrap utility selectors in `:where()` to lower specificity to 0 |
|
|
345
|
+
| `scales` | object | `{}` | Pre-defined scales for cross-file usage (see below) |
|
|
346
|
+
|
|
347
|
+
### Pre-defined Scales (for Astro, Vite, etc.)
|
|
348
|
+
|
|
349
|
+
When using bundlers like Astro or Vite that process CSS files in unpredictable order, you may encounter issues where `@ruler utility()` in one file can't reference a scale defined with `@ruler scale()` in another file.
|
|
350
|
+
|
|
351
|
+
To solve this, define your scales in the plugin config:
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
// postcss.config.js
|
|
355
|
+
module.exports = {
|
|
356
|
+
plugins: {
|
|
357
|
+
"postcss-ruler": {
|
|
358
|
+
scales: {
|
|
359
|
+
space: {
|
|
360
|
+
pairs: {
|
|
361
|
+
xs: [8, 16],
|
|
362
|
+
sm: [16, 24],
|
|
363
|
+
md: [24, 32],
|
|
364
|
+
lg: [32, 48],
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
size: {
|
|
368
|
+
minWidth: 400,
|
|
369
|
+
maxWidth: 1200,
|
|
370
|
+
generateAllCrossPairs: true,
|
|
371
|
+
pairs: {
|
|
372
|
+
sm: [100, 200],
|
|
373
|
+
md: [200, 400],
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Now you can use `@ruler utility()` in any file without needing `@ruler scale()` first:
|
|
383
|
+
|
|
384
|
+
```css
|
|
385
|
+
/* Component.astro or any CSS file */
|
|
386
|
+
@ruler utility({
|
|
387
|
+
selector: '.gap',
|
|
388
|
+
property: 'gap',
|
|
389
|
+
scale: 'space'
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Note:** If you use `@ruler scale()` with the same prefix as a config-defined scale, the inline scale will override the config scale for that file.
|
|
242
394
|
|
|
243
395
|
### At-Rule Options
|
|
244
396
|
|
|
@@ -308,12 +460,13 @@ When min and max values are equal, postcss-ruler outputs a simple rem value inst
|
|
|
308
460
|
|
|
309
461
|
### Utility Options
|
|
310
462
|
|
|
311
|
-
| Option | Type | Default | Description
|
|
312
|
-
| ----------------------- | --------------- | -------- |
|
|
313
|
-
| `selector` | string | required | Any valid CSS selector pattern (e.g., `.gap`, `&.active`, `#section`)
|
|
314
|
-
| `property` | string or array | required | CSS property name(s) to apply the scale values to
|
|
315
|
-
| `scale` | string | required | Name of a previously defined scale (the `prefix` value)
|
|
316
|
-
| `generateAllCrossPairs` | boolean | No | Include/exclude cross-pairs (overrides scale default)
|
|
463
|
+
| Option | Type | Default | Description |
|
|
464
|
+
| ----------------------- | --------------- | -------- | --------------------------------------------------------------------------------- |
|
|
465
|
+
| `selector` | string | required | Any valid CSS selector pattern (e.g., `.gap`, `&.active`, `#section`) |
|
|
466
|
+
| `property` | string or array | required | CSS property name(s) to apply the scale values to |
|
|
467
|
+
| `scale` | string | required | Name of a previously defined scale (the `prefix` value) |
|
|
468
|
+
| `generateAllCrossPairs` | boolean | No | Include/exclude cross-pairs (overrides scale default) |
|
|
469
|
+
| `lowSpecificity` | boolean | No | Wrap selectors in `:where()` to reduce specificity to 0 (overrides global config) |
|
|
317
470
|
|
|
318
471
|
## How It Works
|
|
319
472
|
|
package/index.js
CHANGED
|
@@ -8,6 +8,8 @@ module.exports = (opts) => {
|
|
|
8
8
|
minWidth: 320,
|
|
9
9
|
maxWidth: 1760,
|
|
10
10
|
generateAllCrossPairs: false,
|
|
11
|
+
lowSpecificity: false,
|
|
12
|
+
scales: {},
|
|
11
13
|
};
|
|
12
14
|
const config = Object.assign(DEFAULTS, opts);
|
|
13
15
|
|
|
@@ -111,6 +113,35 @@ module.exports = (opts) => {
|
|
|
111
113
|
return clampScales;
|
|
112
114
|
};
|
|
113
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Pre-processes scales defined in plugin config
|
|
118
|
+
* @param {Object} configScales - Scales object from plugin options
|
|
119
|
+
*/
|
|
120
|
+
const initializeConfigScales = (configScales) => {
|
|
121
|
+
Object.entries(configScales).forEach(([prefix, scaleConfig]) => {
|
|
122
|
+
const scaleOpts = {
|
|
123
|
+
minWidth: scaleConfig.minWidth || config.minWidth,
|
|
124
|
+
maxWidth: scaleConfig.maxWidth || config.maxWidth,
|
|
125
|
+
generateAllCrossPairs:
|
|
126
|
+
scaleConfig.generateAllCrossPairs ?? config.generateAllCrossPairs,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const clampPairs = Object.entries(scaleConfig.pairs).map(
|
|
130
|
+
([name, values]) => ({ name, values }),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
scales[prefix] = generateClamps({
|
|
134
|
+
pairs: clampPairs,
|
|
135
|
+
...scaleOpts,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Initialize scales from config (if any)
|
|
141
|
+
if (Object.keys(config.scales).length > 0) {
|
|
142
|
+
initializeConfigScales(config.scales);
|
|
143
|
+
}
|
|
144
|
+
|
|
114
145
|
/**
|
|
115
146
|
* Parses parameters from @fluid at-rule
|
|
116
147
|
* @param {Array} params - Parsed parameter nodes
|
|
@@ -202,6 +233,7 @@ module.exports = (opts) => {
|
|
|
202
233
|
scale: null,
|
|
203
234
|
generateAllCrossPairs: null,
|
|
204
235
|
attribute: null,
|
|
236
|
+
lowSpecificity: null,
|
|
205
237
|
};
|
|
206
238
|
|
|
207
239
|
for (let i = 0; i < params.length; i++) {
|
|
@@ -241,6 +273,10 @@ module.exports = (opts) => {
|
|
|
241
273
|
utilityParams.generateAllCrossPairs = value === "true";
|
|
242
274
|
i++;
|
|
243
275
|
break;
|
|
276
|
+
case "lowSpecificity":
|
|
277
|
+
utilityParams.lowSpecificity = value === "true";
|
|
278
|
+
i++;
|
|
279
|
+
break;
|
|
244
280
|
}
|
|
245
281
|
}
|
|
246
282
|
|
|
@@ -309,16 +345,21 @@ module.exports = (opts) => {
|
|
|
309
345
|
|
|
310
346
|
const utilityParams = parseUtilityParams(params);
|
|
311
347
|
|
|
348
|
+
// Resolve lowSpecificity from config if not explicitly set
|
|
349
|
+
if (utilityParams.lowSpecificity === null) {
|
|
350
|
+
utilityParams.lowSpecificity = config.lowSpecificity;
|
|
351
|
+
}
|
|
352
|
+
|
|
312
353
|
// Validate attribute-specific constraints first
|
|
313
354
|
if (utilityParams.attribute !== null) {
|
|
314
355
|
if (utilityParams.attribute === "") {
|
|
315
356
|
throw new Error(
|
|
316
|
-
|
|
357
|
+
"[postcss-ruler] @ruler utility() attribute parameter cannot be empty",
|
|
317
358
|
);
|
|
318
359
|
}
|
|
319
360
|
if (!/^[a-zA-Z0-9_-]+$/.test(utilityParams.attribute)) {
|
|
320
361
|
throw new Error(
|
|
321
|
-
|
|
362
|
+
"[postcss-ruler] @ruler utility() attribute parameter must contain only letters, numbers, hyphens, and underscores",
|
|
322
363
|
);
|
|
323
364
|
}
|
|
324
365
|
}
|
|
@@ -368,13 +409,31 @@ module.exports = (opts) => {
|
|
|
368
409
|
if (utilityParams.attribute) {
|
|
369
410
|
// Attribute mode: [data-attr="value"] or .class[data-attr="value"]
|
|
370
411
|
const attrSelector = `[${utilityParams.attribute}="${item.label}"]`;
|
|
371
|
-
|
|
412
|
+
const baseSelector = utilityParams.selector
|
|
372
413
|
? `${utilityParams.selector}${attrSelector}`
|
|
373
414
|
: attrSelector;
|
|
415
|
+
ruleSelector = utilityParams.lowSpecificity
|
|
416
|
+
? `:where(${baseSelector})`
|
|
417
|
+
: baseSelector;
|
|
374
418
|
ruleValue = `var(--${utilityParams.scale}-${item.label})`;
|
|
375
419
|
} else {
|
|
376
420
|
// Class mode (existing behavior)
|
|
377
|
-
|
|
421
|
+
const baseSelector = `${utilityParams.selector}-${item.label}`;
|
|
422
|
+
|
|
423
|
+
// Handle parent context selectors (e.g., ".container &")
|
|
424
|
+
if (utilityParams.lowSpecificity) {
|
|
425
|
+
if (utilityParams.selector.endsWith(" &")) {
|
|
426
|
+
// Parent context: ".container &" -> ".container :where(&-xs)"
|
|
427
|
+
const parentPart = utilityParams.selector.slice(0, -1); // Remove trailing "&"
|
|
428
|
+
ruleSelector = `${parentPart}:where(&-${item.label})`;
|
|
429
|
+
} else {
|
|
430
|
+
// Regular selector: wrap entire selector
|
|
431
|
+
ruleSelector = `:where(${baseSelector})`;
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
ruleSelector = baseSelector;
|
|
435
|
+
}
|
|
436
|
+
|
|
378
437
|
ruleValue = item.clamp;
|
|
379
438
|
}
|
|
380
439
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-ruler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "PostCSS plugin to generate fluid scales and values.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"eslintConfig": {
|
|
39
39
|
"parserOptions": {
|
|
40
|
-
"ecmaVersion":
|
|
40
|
+
"ecmaVersion": 2020
|
|
41
41
|
},
|
|
42
42
|
"env": {
|
|
43
43
|
"node": true,
|