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.
Files changed (3) hide show
  1. package/README.md +105 -11
  2. package/index.js +65 -5
  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
 
@@ -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 a "selector" parameter',
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
- const selector = `${utilityParams.selector}-${item.label}`;
350
- const rule = postcss.rule({ selector });
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: item.clamp }));
413
+ rule.append(postcss.decl({ prop, value: ruleValue }));
354
414
  });
355
415
 
356
416
  return rule;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-ruler",
3
- "version": "1.2.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": [