stylelint-plugin-rhythmguard 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.2.0] - 2026-02-17
10
+
11
+ ### Added
12
+
13
+ - Tailwind integration guidance in README:
14
+ - exact enforcement boundary (CSS declarations vs class strings)
15
+ - recommended layered setup with `stylelint-config-tailwindcss`, `eslint-plugin-tailwindcss`, and `prettier-plugin-tailwindcss`
16
+ - architecture direction for thorough Tailwind coverage.
17
+ - New shared config entry point: `stylelint-plugin-rhythmguard/configs/tailwind`.
18
+ - Tailwind-oriented test coverage for transform token functions and nested translate values.
19
+
20
+ ### Changed
21
+
22
+ - Hardened transform translate parsing to handle nested function values consistently.
23
+ - `use-scale` and `no-offscale-transform` now respect `enforceInsideMathFunctions` in transform translation contexts.
24
+
9
25
  ## [1.1.0] - 2026-02-17
10
26
 
11
27
  ### Added
package/README.md CHANGED
@@ -61,10 +61,20 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
61
61
  }
62
62
  ```
63
63
 
64
+ ### Tailwind config
65
+
66
+ ```json
67
+ {
68
+ "plugins": ["stylelint-plugin-rhythmguard"],
69
+ "extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
70
+ }
71
+ ```
72
+
64
73
  Stable shared config entry points:
65
74
 
66
75
  - `stylelint-plugin-rhythmguard/configs/recommended`
67
76
  - `stylelint-plugin-rhythmguard/configs/strict`
77
+ - `stylelint-plugin-rhythmguard/configs/tailwind`
68
78
 
69
79
  ### Full custom setup
70
80
 
@@ -317,6 +327,59 @@ Options:
317
327
 
318
328
  `rhythmguard/no-offscale-transform` accepts the same scale options as `rhythmguard/use-scale`, but only for transform translation properties.
319
329
 
330
+ ## Tailwind CSS Integration
331
+
332
+ Rhythmguard works well in Tailwind projects, but it enforces what Stylelint can parse: CSS declarations.
333
+
334
+ ### What Rhythmguard covers in Tailwind projects
335
+
336
+ - custom CSS in `globals.css`, `components.css`, `utilities.css`
337
+ - CSS Modules (for example `*.module.css`)
338
+ - declarations inside `@layer` blocks
339
+
340
+ ### What Rhythmguard does not cover
341
+
342
+ - Tailwind class strings in templates/JSX/TSX, for example:
343
+ - `class="p-4 gap-2"`
344
+ - `class="p-[13px] translate-y-[18px]"`
345
+
346
+ Those are not Stylelint declaration nodes, so they are outside this plugin's scope.
347
+
348
+ ### Recommended stack for full Tailwind enforcement
349
+
350
+ Use both layers:
351
+
352
+ 1. Stylelint + Rhythmguard for CSS declaration governance.
353
+ 2. Tailwind-aware class-string linting/formatting for template utility usage.
354
+
355
+ Suggested setup:
356
+
357
+ ```json
358
+ {
359
+ "extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
360
+ }
361
+ ```
362
+
363
+ Then pair with:
364
+
365
+ - `eslint-plugin-tailwindcss` for class-string rules (including arbitrary-value governance).
366
+ - `prettier-plugin-tailwindcss` for deterministic class ordering.
367
+
368
+ Detailed setup reference: [`docs/TAILWIND.md`](./docs/TAILWIND.md).
369
+
370
+ ### Tailwind token function support
371
+
372
+ By default, `tokenFunctions` includes `theme`, so values like `theme(spacing.4)` are treated as tokenized values.
373
+
374
+ ### Product direction
375
+
376
+ We should extend Tailwind coverage thoroughly, but in the right architecture:
377
+
378
+ - keep `stylelint-plugin-rhythmguard` focused on CSS declaration enforcement
379
+ - add a complementary Tailwind class-string layer (ESLint/plugin side) for utility classes
380
+
381
+ This avoids brittle parsing hacks and gives full coverage without compromising rule quality.
382
+
320
383
  ## Programmatic Presets
321
384
 
322
385
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Stylelint plugin for spacing scale and token enforcement",
5
5
  "keywords": [
6
6
  "stylelint",
@@ -18,6 +18,7 @@
18
18
  ".": "./src/index.js",
19
19
  "./configs/recommended": "./src/configs/recommended.js",
20
20
  "./configs/strict": "./src/configs/strict.js",
21
+ "./configs/tailwind": "./src/configs/tailwind.js",
21
22
  "./presets": "./src/presets/index.js",
22
23
  "./rules/use-scale": "./src/rules/use-scale/index.js",
23
24
  "./rules/prefer-token": "./src/rules/prefer-token/index.js",
@@ -59,6 +60,9 @@
59
60
  "peerDependencies": {
60
61
  "stylelint": "^16.0.0"
61
62
  },
63
+ "dependencies": {
64
+ "stylelint-config-tailwindcss": "^1.0.1"
65
+ },
62
66
  "devDependencies": {
63
67
  "@eslint/js": "^9.22.0",
64
68
  "c8": "^10.1.3",
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ extends: [
5
+ 'stylelint-config-tailwindcss',
6
+ 'stylelint-plugin-rhythmguard/configs/strict',
7
+ ],
8
+ };
package/src/index.js CHANGED
@@ -15,5 +15,6 @@ module.exports.rules = {
15
15
  module.exports.configs = {
16
16
  recommended: require('./configs/recommended'),
17
17
  strict: require('./configs/strict'),
18
+ tailwind: require('./configs/tailwind'),
18
19
  };
19
20
  module.exports.presets = require('./presets');
@@ -14,6 +14,8 @@ const {
14
14
  const { buildScaleOptions } = require('../../utils/options');
15
15
  const {
16
16
  declarationValueIndex,
17
+ isMathFunction,
18
+ walkRootValueNodes,
17
19
  walkTransformTranslateNodes,
18
20
  } = require('../../utils/value-utils');
19
21
 
@@ -138,15 +140,55 @@ const ruleFunction = (primary, secondaryOptions) => {
138
140
  };
139
141
 
140
142
  if (prop === 'transform') {
141
- walkTransformTranslateNodes(parsed, (node) => {
143
+ walkTransformTranslateNodes(parsed, (node, parentFunctionName) => {
144
+ if (node.type === 'function') {
145
+ if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
146
+ return true;
147
+ }
148
+
149
+ return false;
150
+ }
151
+
152
+ if (node.type !== 'word') {
153
+ return false;
154
+ }
155
+
156
+ if (
157
+ parentFunctionName &&
158
+ isMathFunction(parentFunctionName) &&
159
+ !options.enforceInsideMathFunctions
160
+ ) {
161
+ return false;
162
+ }
163
+
142
164
  checkNode(node);
165
+ return false;
143
166
  });
144
167
  } else {
145
- for (const node of parsed.nodes) {
146
- if (node.type === 'word') {
147
- checkNode(node);
168
+ walkRootValueNodes(parsed, (node, parentFunctionName) => {
169
+ if (node.type === 'function') {
170
+ if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
171
+ return true;
172
+ }
173
+
174
+ return false;
148
175
  }
149
- }
176
+
177
+ if (node.type !== 'word') {
178
+ return false;
179
+ }
180
+
181
+ if (
182
+ parentFunctionName &&
183
+ isMathFunction(parentFunctionName) &&
184
+ !options.enforceInsideMathFunctions
185
+ ) {
186
+ return false;
187
+ }
188
+
189
+ checkNode(node);
190
+ return false;
191
+ });
150
192
  }
151
193
 
152
194
  if (changed) {
@@ -142,8 +142,25 @@ const ruleFunction = (primary, secondaryOptions) => {
142
142
  };
143
143
 
144
144
  if (prop === 'transform') {
145
- walkTransformTranslateNodes(parsed, (node) => {
146
- changed = checkWordNode(node) || changed;
145
+ walkTransformTranslateNodes(parsed, (node, parentFunctionName) => {
146
+ if (node.type === 'function') {
147
+ if (isTokenFunction(node, options.tokenFunctions, tokenRegex)) {
148
+ return true;
149
+ }
150
+
151
+ if (isMathFunction(node.value)) {
152
+ return true;
153
+ }
154
+
155
+ return false;
156
+ }
157
+
158
+ if (node.type !== 'word') {
159
+ return false;
160
+ }
161
+
162
+ changed = checkWordNode(node, parentFunctionName) || changed;
163
+ return false;
147
164
  });
148
165
  } else {
149
166
  walkRootValueNodes(parsed, (node, parentFunctionName) => {
@@ -170,7 +170,35 @@ const ruleFunction = (primary, secondaryOptions) => {
170
170
  let changed = false;
171
171
 
172
172
  if (prop === 'transform') {
173
- walkTransformTranslateNodes(parsed, (node) => {
173
+ walkTransformTranslateNodes(parsed, (node, parentFunctionName) => {
174
+ if (node.type === 'function') {
175
+ if (isTokenFunction(node, options.tokenFunctions, tokenRegex)) {
176
+ return true;
177
+ }
178
+
179
+ if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
180
+ return true;
181
+ }
182
+
183
+ return false;
184
+ }
185
+
186
+ if (node.type !== 'word') {
187
+ return false;
188
+ }
189
+
190
+ if (isKeyword(node.value, options.ignoreValues)) {
191
+ return false;
192
+ }
193
+
194
+ if (
195
+ parentFunctionName &&
196
+ isMathFunction(parentFunctionName) &&
197
+ !options.enforceInsideMathFunctions
198
+ ) {
199
+ return false;
200
+ }
201
+
174
202
  changed =
175
203
  checkLengthValue({
176
204
  decl,
@@ -179,6 +207,8 @@ const ruleFunction = (primary, secondaryOptions) => {
179
207
  report,
180
208
  scalePx,
181
209
  }) || changed;
210
+
211
+ return false;
182
212
  });
183
213
  } else {
184
214
  walkRootValueNodes(parsed, (node, parentFunctionName) => {
@@ -77,6 +77,23 @@ function walkRootValueNodes(parsed, walkNode, state) {
77
77
  }
78
78
 
79
79
  function walkTransformTranslateNodes(parsed, walkNode) {
80
+ const walkNodes = (nodes, parentFunctionName) => {
81
+ for (const node of nodes) {
82
+ if (node.type === 'function') {
83
+ const fnName = node.value.toLowerCase();
84
+ const skipChildren = walkNode(node, parentFunctionName);
85
+ if (skipChildren) {
86
+ continue;
87
+ }
88
+
89
+ walkNodes(node.nodes, fnName);
90
+ continue;
91
+ }
92
+
93
+ walkNode(node, parentFunctionName);
94
+ }
95
+ };
96
+
80
97
  for (const node of parsed.nodes) {
81
98
  if (node.type !== 'function') {
82
99
  continue;
@@ -86,11 +103,7 @@ function walkTransformTranslateNodes(parsed, walkNode) {
86
103
  continue;
87
104
  }
88
105
 
89
- for (const child of node.nodes) {
90
- if (child.type === 'word') {
91
- walkNode(child, node.value.toLowerCase());
92
- }
93
- }
106
+ walkNodes(node.nodes, node.value.toLowerCase());
94
107
  }
95
108
  }
96
109