postcss-ruler 1.3.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.
Files changed (3) hide show
  1. package/README.md +105 -11
  2. package/index.js +33 -4
  3. 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
 
@@ -202,6 +203,7 @@ module.exports = (opts) => {
202
203
  scale: null,
203
204
  generateAllCrossPairs: null,
204
205
  attribute: null,
206
+ lowSpecificity: null,
205
207
  };
206
208
 
207
209
  for (let i = 0; i < params.length; i++) {
@@ -241,6 +243,10 @@ module.exports = (opts) => {
241
243
  utilityParams.generateAllCrossPairs = value === "true";
242
244
  i++;
243
245
  break;
246
+ case "lowSpecificity":
247
+ utilityParams.lowSpecificity = value === "true";
248
+ i++;
249
+ break;
244
250
  }
245
251
  }
246
252
 
@@ -309,16 +315,21 @@ module.exports = (opts) => {
309
315
 
310
316
  const utilityParams = parseUtilityParams(params);
311
317
 
318
+ // Resolve lowSpecificity from config if not explicitly set
319
+ if (utilityParams.lowSpecificity === null) {
320
+ utilityParams.lowSpecificity = config.lowSpecificity;
321
+ }
322
+
312
323
  // Validate attribute-specific constraints first
313
324
  if (utilityParams.attribute !== null) {
314
325
  if (utilityParams.attribute === "") {
315
326
  throw new Error(
316
- '[postcss-ruler] @ruler utility() attribute parameter cannot be empty',
327
+ "[postcss-ruler] @ruler utility() attribute parameter cannot be empty",
317
328
  );
318
329
  }
319
330
  if (!/^[a-zA-Z0-9_-]+$/.test(utilityParams.attribute)) {
320
331
  throw new Error(
321
- '[postcss-ruler] @ruler utility() attribute parameter must contain only letters, numbers, hyphens, and underscores',
332
+ "[postcss-ruler] @ruler utility() attribute parameter must contain only letters, numbers, hyphens, and underscores",
322
333
  );
323
334
  }
324
335
  }
@@ -368,13 +379,31 @@ module.exports = (opts) => {
368
379
  if (utilityParams.attribute) {
369
380
  // Attribute mode: [data-attr="value"] or .class[data-attr="value"]
370
381
  const attrSelector = `[${utilityParams.attribute}="${item.label}"]`;
371
- ruleSelector = utilityParams.selector
382
+ const baseSelector = utilityParams.selector
372
383
  ? `${utilityParams.selector}${attrSelector}`
373
384
  : attrSelector;
385
+ ruleSelector = utilityParams.lowSpecificity
386
+ ? `:where(${baseSelector})`
387
+ : baseSelector;
374
388
  ruleValue = `var(--${utilityParams.scale}-${item.label})`;
375
389
  } else {
376
390
  // Class mode (existing behavior)
377
- ruleSelector = `${utilityParams.selector}-${item.label}`;
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
+
378
407
  ruleValue = item.clamp;
379
408
  }
380
409
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-ruler",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "PostCSS plugin to generate fluid scales and values.",
5
5
  "main": "index.js",
6
6
  "files": [