postcss-ruler 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -11
- package/index.js +65 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,15 +230,108 @@ Convert individual values directly to `clamp()` functions:
|
|
|
230
230
|
}
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
+
### 4. Low Specificity Mode: Zero-Specificity Utilities
|
|
234
|
+
|
|
235
|
+
Wrap generated selectors in `:where()` to reduce their specificity to 0, making them easier to override:
|
|
236
|
+
|
|
237
|
+
```css
|
|
238
|
+
@ruler scale({
|
|
239
|
+
prefix: 'space',
|
|
240
|
+
pairs: {
|
|
241
|
+
"xs": [8, 16],
|
|
242
|
+
"sm": [16, 24]
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
/* Enable lowSpecificity per utility */
|
|
247
|
+
@ruler utility({
|
|
248
|
+
selector: '.gap',
|
|
249
|
+
property: 'gap',
|
|
250
|
+
scale: 'space',
|
|
251
|
+
lowSpecificity: true
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Generates:**
|
|
256
|
+
|
|
257
|
+
```css
|
|
258
|
+
--space-xs: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem);
|
|
259
|
+
--space-sm: clamp(1rem, 0.5556vw + 0.8889rem, 1.5rem);
|
|
260
|
+
|
|
261
|
+
:where(.gap-xs) {
|
|
262
|
+
gap: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem);
|
|
263
|
+
}
|
|
264
|
+
:where(.gap-sm) {
|
|
265
|
+
gap: clamp(1rem, 0.5556vw + 0.8889rem, 1.5rem);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Specificity comparison:**
|
|
270
|
+
|
|
271
|
+
- `.gap-xs` → specificity (0,1,0)
|
|
272
|
+
- `:where(.gap-xs)` → specificity (0,0,0)
|
|
273
|
+
|
|
274
|
+
**Use case:** Design system utilities that should be easily overridable without `!important`:
|
|
275
|
+
|
|
276
|
+
```css
|
|
277
|
+
/* Utility with zero specificity */
|
|
278
|
+
:where(.gap-md) {
|
|
279
|
+
gap: clamp(1.5rem, 0.5556vw + 1.3889rem, 2rem);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Easy to override with a simple class */
|
|
283
|
+
.custom-layout {
|
|
284
|
+
gap: 2rem; /* This wins without !important */
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Works with attribute mode:**
|
|
289
|
+
|
|
290
|
+
```css
|
|
291
|
+
@ruler utility({
|
|
292
|
+
attribute: 'data-size',
|
|
293
|
+
property: 'font-size',
|
|
294
|
+
scale: 'size',
|
|
295
|
+
lowSpecificity: true
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Generates:**
|
|
300
|
+
|
|
301
|
+
```css
|
|
302
|
+
:where([data-size="xs"]) {
|
|
303
|
+
font-size: var(--size-xs);
|
|
304
|
+
}
|
|
305
|
+
:where([data-size="sm"]) {
|
|
306
|
+
font-size: var(--size-sm);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Global configuration:**
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
// postcss.config.js
|
|
314
|
+
module.exports = {
|
|
315
|
+
plugins: {
|
|
316
|
+
"postcss-ruler": {
|
|
317
|
+
lowSpecificity: true, // All utilities use :where() by default
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
You can override the global setting per utility by explicitly setting `lowSpecificity: false`.
|
|
324
|
+
|
|
233
325
|
## Configuration Options
|
|
234
326
|
|
|
235
327
|
### Plugin Options
|
|
236
328
|
|
|
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
|
|
329
|
+
| Option | Type | Default | Description |
|
|
330
|
+
| ----------------------- | ------- | ------- | -------------------------------------------------------------- |
|
|
331
|
+
| `minWidth` | number | `320` | Default minimum viewport width in pixels |
|
|
332
|
+
| `maxWidth` | number | `1760` | Default maximum viewport width in pixels |
|
|
333
|
+
| `generateAllCrossPairs` | boolean | `false` | Generate cross-combinations in scale mode |
|
|
334
|
+
| `lowSpecificity` | boolean | `false` | Wrap utility selectors in `:where()` to lower specificity to 0 |
|
|
242
335
|
|
|
243
336
|
### At-Rule Options
|
|
244
337
|
|
|
@@ -308,12 +401,13 @@ When min and max values are equal, postcss-ruler outputs a simple rem value inst
|
|
|
308
401
|
|
|
309
402
|
### Utility Options
|
|
310
403
|
|
|
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)
|
|
404
|
+
| Option | Type | Default | Description |
|
|
405
|
+
| ----------------------- | --------------- | -------- | --------------------------------------------------------------------------------- |
|
|
406
|
+
| `selector` | string | required | Any valid CSS selector pattern (e.g., `.gap`, `&.active`, `#section`) |
|
|
407
|
+
| `property` | string or array | required | CSS property name(s) to apply the scale values to |
|
|
408
|
+
| `scale` | string | required | Name of a previously defined scale (the `prefix` value) |
|
|
409
|
+
| `generateAllCrossPairs` | boolean | No | Include/exclude cross-pairs (overrides scale default) |
|
|
410
|
+
| `lowSpecificity` | boolean | No | Wrap selectors in `:where()` to reduce specificity to 0 (overrides global config) |
|
|
317
411
|
|
|
318
412
|
## How It Works
|
|
319
413
|
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ module.exports = (opts) => {
|
|
|
8
8
|
minWidth: 320,
|
|
9
9
|
maxWidth: 1760,
|
|
10
10
|
generateAllCrossPairs: false,
|
|
11
|
+
lowSpecificity: false,
|
|
11
12
|
};
|
|
12
13
|
const config = Object.assign(DEFAULTS, opts);
|
|
13
14
|
|
|
@@ -201,6 +202,8 @@ module.exports = (opts) => {
|
|
|
201
202
|
property: null,
|
|
202
203
|
scale: null,
|
|
203
204
|
generateAllCrossPairs: null,
|
|
205
|
+
attribute: null,
|
|
206
|
+
lowSpecificity: null,
|
|
204
207
|
};
|
|
205
208
|
|
|
206
209
|
for (let i = 0; i < params.length; i++) {
|
|
@@ -213,6 +216,7 @@ module.exports = (opts) => {
|
|
|
213
216
|
switch (key) {
|
|
214
217
|
case "selector":
|
|
215
218
|
case "scale":
|
|
219
|
+
case "attribute":
|
|
216
220
|
utilityParams[key] = value.replace(/['"]/g, "");
|
|
217
221
|
i++;
|
|
218
222
|
break;
|
|
@@ -239,6 +243,10 @@ module.exports = (opts) => {
|
|
|
239
243
|
utilityParams.generateAllCrossPairs = value === "true";
|
|
240
244
|
i++;
|
|
241
245
|
break;
|
|
246
|
+
case "lowSpecificity":
|
|
247
|
+
utilityParams.lowSpecificity = value === "true";
|
|
248
|
+
i++;
|
|
249
|
+
break;
|
|
242
250
|
}
|
|
243
251
|
}
|
|
244
252
|
|
|
@@ -307,10 +315,29 @@ module.exports = (opts) => {
|
|
|
307
315
|
|
|
308
316
|
const utilityParams = parseUtilityParams(params);
|
|
309
317
|
|
|
318
|
+
// Resolve lowSpecificity from config if not explicitly set
|
|
319
|
+
if (utilityParams.lowSpecificity === null) {
|
|
320
|
+
utilityParams.lowSpecificity = config.lowSpecificity;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Validate attribute-specific constraints first
|
|
324
|
+
if (utilityParams.attribute !== null) {
|
|
325
|
+
if (utilityParams.attribute === "") {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"[postcss-ruler] @ruler utility() attribute parameter cannot be empty",
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(utilityParams.attribute)) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
"[postcss-ruler] @ruler utility() attribute parameter must contain only letters, numbers, hyphens, and underscores",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
310
337
|
// Validate required parameters
|
|
311
|
-
if (!utilityParams.selector) {
|
|
338
|
+
if (!utilityParams.selector && !utilityParams.attribute) {
|
|
312
339
|
throw new Error(
|
|
313
|
-
'[postcss-ruler] @ruler utility() requires
|
|
340
|
+
'[postcss-ruler] @ruler utility() requires either "selector" or "attribute" parameter',
|
|
314
341
|
);
|
|
315
342
|
}
|
|
316
343
|
if (!utilityParams.property) {
|
|
@@ -346,11 +373,44 @@ module.exports = (opts) => {
|
|
|
346
373
|
|
|
347
374
|
// Generate utility classes as PostCSS nodes
|
|
348
375
|
const rules = scaleItems.map((item) => {
|
|
349
|
-
|
|
350
|
-
|
|
376
|
+
let ruleSelector;
|
|
377
|
+
let ruleValue;
|
|
378
|
+
|
|
379
|
+
if (utilityParams.attribute) {
|
|
380
|
+
// Attribute mode: [data-attr="value"] or .class[data-attr="value"]
|
|
381
|
+
const attrSelector = `[${utilityParams.attribute}="${item.label}"]`;
|
|
382
|
+
const baseSelector = utilityParams.selector
|
|
383
|
+
? `${utilityParams.selector}${attrSelector}`
|
|
384
|
+
: attrSelector;
|
|
385
|
+
ruleSelector = utilityParams.lowSpecificity
|
|
386
|
+
? `:where(${baseSelector})`
|
|
387
|
+
: baseSelector;
|
|
388
|
+
ruleValue = `var(--${utilityParams.scale}-${item.label})`;
|
|
389
|
+
} else {
|
|
390
|
+
// Class mode (existing behavior)
|
|
391
|
+
const baseSelector = `${utilityParams.selector}-${item.label}`;
|
|
392
|
+
|
|
393
|
+
// Handle parent context selectors (e.g., ".container &")
|
|
394
|
+
if (utilityParams.lowSpecificity) {
|
|
395
|
+
if (utilityParams.selector.endsWith(" &")) {
|
|
396
|
+
// Parent context: ".container &" -> ".container :where(&-xs)"
|
|
397
|
+
const parentPart = utilityParams.selector.slice(0, -1); // Remove trailing "&"
|
|
398
|
+
ruleSelector = `${parentPart}:where(&-${item.label})`;
|
|
399
|
+
} else {
|
|
400
|
+
// Regular selector: wrap entire selector
|
|
401
|
+
ruleSelector = `:where(${baseSelector})`;
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
ruleSelector = baseSelector;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
ruleValue = item.clamp;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const rule = postcss.rule({ selector: ruleSelector });
|
|
351
411
|
|
|
352
412
|
properties.forEach((prop) => {
|
|
353
|
-
rule.append(postcss.decl({ prop, value:
|
|
413
|
+
rule.append(postcss.decl({ prop, value: ruleValue }));
|
|
354
414
|
});
|
|
355
415
|
|
|
356
416
|
return rule;
|