postcss-ruler 1.0.1 → 1.1.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 +141 -2
  2. package/index.js +144 -10
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -37,7 +37,7 @@ module.exports = {
37
37
 
38
38
  ## Features
39
39
 
40
- ### 1. At-Rule Mode: Generate Fluid Scale
40
+ ### 1. Scale Generation: Create Fluid Scales
41
41
 
42
42
  Create multiple CSS custom properties from named size pairs:
43
43
  ```css
@@ -106,7 +106,81 @@ For example, if you have `xs: [8, 16]` and `lg: [32, 48]`, a cross pair `xs-lg`
106
106
  }
107
107
  ```
108
108
 
109
- ### 2. Inline Mode: Fluid Function
109
+ ### 2. Utility Class Generation: Auto-Generate Utility Classes
110
+
111
+ Generate utility classes from your defined scales with complete selector flexibility:
112
+
113
+ ```css
114
+ @ruler scale({
115
+ prefix: 'space',
116
+ pairs: {
117
+ "xs": [8, 16],
118
+ "sm": [16, 24],
119
+ "md": [24, 32]
120
+ }
121
+ });
122
+
123
+ /* Basic class selector */
124
+ @ruler utility({
125
+ selector: '.gap',
126
+ property: 'gap',
127
+ scale: 'space'
128
+ });
129
+
130
+ /* Nested selector with & (for PostCSS nesting) */
131
+ @ruler utility({
132
+ selector: '&.active',
133
+ property: 'padding',
134
+ scale: 'space'
135
+ });
136
+
137
+ /* Multiple properties */
138
+ @ruler utility({
139
+ selector: '.p-block',
140
+ property: ['padding-top', 'padding-bottom'],
141
+ scale: 'space'
142
+ });
143
+ ```
144
+
145
+ **Generates:**
146
+ ```css
147
+ --space-xs: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem);
148
+ --space-sm: clamp(1rem, 0.5556vw + 0.8889rem, 1.5rem);
149
+ --space-md: clamp(1.5rem, 0.5556vw + 1.3889rem, 2rem);
150
+
151
+ .gap-xs { gap: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem) }
152
+ .gap-sm { gap: clamp(1rem, 0.5556vw + 0.8889rem, 1.5rem) }
153
+ .gap-md { gap: clamp(1.5rem, 0.5556vw + 1.3889rem, 2rem) }
154
+
155
+ &.active-xs { padding: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem) }
156
+ &.active-sm { padding: clamp(1rem, 0.5556vw + 0.8889rem, 1.5rem) }
157
+ &.active-md { padding: clamp(1.5rem, 0.5556vw + 1.3889rem, 2rem) }
158
+
159
+ .p-block-xs {
160
+ padding-top: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem);
161
+ padding-bottom: clamp(0.5rem, 0.5556vw + 0.3889rem, 1rem)
162
+ }
163
+ /* ... */
164
+ ```
165
+
166
+ **Supported selector patterns:**
167
+ - **Class selectors**: `.gap` → `.gap-xs`, `.gap-sm`, `.gap-md`
168
+ - **Nested with &**: `&.active` → `&.active-xs`, `&.active-sm` (PostCSS nesting)
169
+ - **Multiple classes**: `.container.space` → `.container.space-xs`, etc.
170
+ - **ID selectors**: `#section` → `#section-xs`, `#section-sm`
171
+ - **Element selectors**: `section` → `section-xs`, `section-sm`
172
+ - **Parent context**: `.container &` → `.container &-xs`, etc.
173
+
174
+ **Utility options:**
175
+
176
+ | Option | Type | Required | Description |
177
+ |--------|------|----------|-------------|
178
+ | `selector` | string | Yes | Any valid CSS selector pattern (e.g., `.gap`, `&.active`, `#section`) |
179
+ | `property` | string or array | Yes | CSS property name(s) to apply the scale values to |
180
+ | `scale` | string | Yes | Name of a previously defined scale (the `prefix` value) |
181
+ | `generateAllCrossPairs` | boolean | No | Include/exclude cross-pairs (overrides scale default) |
182
+
183
+ ### 3. Inline Mode: Fluid Function
110
184
 
111
185
  Convert individual values directly to `clamp()` functions:
112
186
  ```css
@@ -166,6 +240,15 @@ ruler.fluid(minSize, maxSize[, minWidth, maxWidth])
166
240
  | `minWidth` | number | No | Minimum viewport width (uses config default) |
167
241
  | `maxWidth` | number | No | Maximum viewport width (uses config default) |
168
242
 
243
+ ### Utility Options
244
+
245
+ | Option | Type | Default | Description |
246
+ |--------|------|---------|-------------|
247
+ | `selector` | string | required | Any valid CSS selector pattern (e.g., `.gap`, `&.active`, `#section`) |
248
+ | `property` | string or array | required | CSS property name(s) to apply the scale values to |
249
+ | `scale` | string | required | Name of a previously defined scale (the `prefix` value) |
250
+ | `generateAllCrossPairs` | boolean | No | Include/exclude cross-pairs (overrides scale default) |
251
+
169
252
  ## How It Works
170
253
 
171
254
  The plugin uses linear interpolation to create fluid values that scale smoothly between viewport sizes:
@@ -189,6 +272,62 @@ At 1760px viewport: `1.5rem` (24px)
189
272
 
190
273
  ## Use Cases
191
274
 
275
+ ### Utility-First Workflow with Fluid Scales
276
+
277
+ Generate a complete set of utility classes from your design system:
278
+
279
+ ```css
280
+ @ruler scale({
281
+ prefix: 'space',
282
+ generateAllCrossPairs: true,
283
+ pairs: {
284
+ "xs": [8, 16],
285
+ "sm": [12, 20],
286
+ "md": [16, 28],
287
+ "lg": [24, 40],
288
+ "xl": [32, 56]
289
+ }
290
+ });
291
+
292
+ /* Gap utilities */
293
+ @ruler utility({
294
+ selector: '.gap',
295
+ property: 'gap',
296
+ scale: 'space'
297
+ });
298
+
299
+ /* Padding utilities */
300
+ @ruler utility({
301
+ selector: '.p',
302
+ property: 'padding',
303
+ scale: 'space'
304
+ });
305
+
306
+ /* Margin utilities */
307
+ @ruler utility({
308
+ selector: '.m',
309
+ property: 'margin',
310
+ scale: 'space'
311
+ });
312
+
313
+ /* Stack spacing (for flow layout) */
314
+ @ruler utility({
315
+ selector: '.stack > * + *',
316
+ property: 'margin-top',
317
+ scale: 'space'
318
+ });
319
+ ```
320
+
321
+ Use in your HTML:
322
+ ```html
323
+ <section class="p-lg gap-md">
324
+ <div class="stack gap-sm">
325
+ <h2>Heading</h2>
326
+ <p>Content that scales smoothly</p>
327
+ </div>
328
+ </section>
329
+ ```
330
+
192
331
  ### Responsive Typography
193
332
 
194
333
  ```css
package/index.js CHANGED
@@ -11,6 +11,9 @@ module.exports = opts => {
11
11
  };
12
12
  const config = Object.assign(DEFAULTS, opts);
13
13
 
14
+ // Storage for generated scales
15
+ const scales = {};
16
+
14
17
  /**
15
18
  * Converts pixels to r]em units
16
19
  * @param {number} px - Pixel value to convert
@@ -113,7 +116,6 @@ module.exports = opts => {
113
116
  minWidth: config.minWidth,
114
117
  maxWidth: config.maxWidth,
115
118
  pairs: {},
116
- relativeTo: 'viewport',
117
119
  prefix: 'space',
118
120
  generateAllCrossPairs: config.generateAllCrossPairs,
119
121
  };
@@ -132,11 +134,7 @@ module.exports = opts => {
132
134
  i++;
133
135
  break;
134
136
  case 'prefix':
135
- clampsParams.prefix = value.replace(/['\"]/g, '');
136
- i++;
137
- break;
138
- case 'relativeTo':
139
- clampsParams.relativeTo = value.replace(/['\"]/g, '');
137
+ clampsParams.prefix = value.replace(/['"]/g, '');
140
138
  i++;
141
139
  break;
142
140
  case 'generateAllCrossPairs':
@@ -187,6 +185,61 @@ module.exports = opts => {
187
185
  return pairs;
188
186
  };
189
187
 
188
+ /**
189
+ * Parses parameters from @ruler utility() at-rule
190
+ * @param {Array} params - Parsed parameter nodes
191
+ * @returns {Object} Parsed configuration object
192
+ */
193
+ const parseUtilityParams = params => {
194
+ const utilityParams = {
195
+ selector: null,
196
+ property: null,
197
+ scale: null,
198
+ generateAllCrossPairs: null,
199
+ };
200
+
201
+ for (let i = 0; i < params.length; i++) {
202
+ const param = params[i];
203
+ const nextParam = params[i + 1];
204
+ if (!param || !nextParam) continue;
205
+ const key = param.value;
206
+ let value = nextParam.value.replace(/[:,]/g, '');
207
+
208
+ switch (key) {
209
+ case 'selector':
210
+ case 'scale':
211
+ utilityParams[key] = value.replace(/['"]/g, '');
212
+ i++;
213
+ break;
214
+ case 'property':
215
+ // Check if it's an array (next token is '[')
216
+ if (nextParam.value === '[') {
217
+ // It's an array - collect values until we hit ']'
218
+ const arrayValues = [];
219
+ i += 2; // Skip 'property' and '['
220
+ while (i < params.length && params[i].value !== ']') {
221
+ if (params[i].type === 'string') {
222
+ arrayValues.push(params[i].value);
223
+ }
224
+ i++;
225
+ }
226
+ utilityParams.property = arrayValues;
227
+ } else {
228
+ // Single value
229
+ utilityParams.property = value.replace(/['"]/g, '');
230
+ i++;
231
+ }
232
+ break;
233
+ case 'generateAllCrossPairs':
234
+ utilityParams.generateAllCrossPairs = value === 'true';
235
+ i++;
236
+ break;
237
+ }
238
+ }
239
+
240
+ return utilityParams;
241
+ };
242
+
190
243
  /**
191
244
  * Processes @fluid at-rule and generates CSS custom properties
192
245
  * @param {Object} atRule - PostCSS at-rule node
@@ -217,11 +270,90 @@ module.exports = opts => {
217
270
  pairs: clampPairs,
218
271
  });
219
272
 
220
- const response = clampScale
221
- .map(step => `--${clampsParams.prefix}-${step.label}: ${step.clamp};`)
222
- .join('\n');
273
+ // Store the scale for later use by utility classes
274
+ scales[clampsParams.prefix] = clampScale;
275
+
276
+ const postcss = require('postcss');
277
+ const root = postcss.root();
278
+
279
+ clampScale.forEach(step => {
280
+ root.append(
281
+ postcss.decl({
282
+ prop: `--${clampsParams.prefix}-${step.label}`,
283
+ value: step.clamp,
284
+ })
285
+ );
286
+ });
287
+
288
+ atRule.replaceWith(root.nodes);
289
+ };
290
+
291
+ /**
292
+ * Processes @ruler utility() at-rule and generates utility classes
293
+ * @param {Object} atRule - PostCSS at-rule node
294
+ */
295
+ const processUtilityAtRule = atRule => {
296
+ const postcss = require('postcss');
297
+ const { nodes } = CSSValueParser(atRule.params);
298
+ const params = nodes[0].nodes.filter(
299
+ x =>
300
+ ['word', 'string', 'function'].includes(x.type) &&
301
+ x.value !== '{' &&
302
+ x.value !== '}'
303
+ );
304
+
305
+ const utilityParams = parseUtilityParams(params);
306
+
307
+ // Validate required parameters
308
+ if (!utilityParams.selector) {
309
+ throw new Error(
310
+ '[postcss-ruler] @ruler utility() requires a "selector" parameter'
311
+ );
312
+ }
313
+ if (!utilityParams.property) {
314
+ throw new Error(
315
+ '[postcss-ruler] @ruler utility() requires a "property" parameter'
316
+ );
317
+ }
318
+ if (!utilityParams.scale) {
319
+ throw new Error(
320
+ '[postcss-ruler] @ruler utility() requires a "scale" parameter'
321
+ );
322
+ }
323
+
324
+ // Check if scale exists
325
+ const scale = scales[utilityParams.scale];
326
+ if (!scale) {
327
+ throw new Error(
328
+ `[postcss-ruler] Scale "${utilityParams.scale}" not found. Define it with @ruler scale() first.`
329
+ );
330
+ }
331
+
332
+ // Determine which scale items to use
333
+ let scaleItems = scale;
334
+ if (utilityParams.generateAllCrossPairs === false) {
335
+ // Filter out cross-pairs (items with hyphens in label)
336
+ scaleItems = scale.filter(item => !item.label.includes('-'));
337
+ }
338
+
339
+ // Normalize property to array
340
+ const properties = Array.isArray(utilityParams.property)
341
+ ? utilityParams.property
342
+ : [utilityParams.property];
343
+
344
+ // Generate utility classes as PostCSS nodes
345
+ const rules = scaleItems.map(item => {
346
+ const selector = `${utilityParams.selector}-${item.label}`;
347
+ const rule = postcss.rule({ selector });
348
+
349
+ properties.forEach(prop => {
350
+ rule.append(postcss.decl({ prop, value: item.clamp }));
351
+ });
352
+
353
+ return rule;
354
+ });
223
355
 
224
- atRule.replaceWith(response);
356
+ atRule.replaceWith(rules);
225
357
  };
226
358
 
227
359
  /**
@@ -267,6 +399,8 @@ module.exports = opts => {
267
399
  ruler: atRule => {
268
400
  if (atRule.params.startsWith('scale(')) {
269
401
  return processFluidAtRule(atRule);
402
+ } else if (atRule.params.startsWith('utility(')) {
403
+ return processUtilityAtRule(atRule);
270
404
  }
271
405
  },
272
406
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-ruler",
3
- "version": "1.0.1",
3
+ "version": "1.1.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": 2017
40
+ "ecmaVersion": 2018
41
41
  },
42
42
  "env": {
43
43
  "node": true,