stylelint-plugin-rhythmguard 1.4.1 → 1.5.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,20 @@ The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.2] - 2026-02-21
10
+
11
+ ### Changed
12
+
13
+ - npm package artifact is now leaner by excluding non-runtime media assets from published files.
14
+ - README media links now use absolute GitHub URLs so npm README rendering remains intact without bundling local media files.
15
+ - Added a dedicated "Drop-In for Existing Projects" path with a single install command and a single config block.
16
+ - Added comparison and migration guidance:
17
+ - `docs/COMPARISON.md` (`defensive-css` vs `logical-css` vs Rhythmguard + migration recipes)
18
+ - `docs/ADOPTION_DIFFS.md` (real before/after excerpts from public codebases)
19
+ - Added Dev.to publishing assets:
20
+ - `docs/DEVTO_ORIGINAL_UPDATE_NOTE_2026-02-21.md`
21
+ - `docs/DEVTO_CONTINUATION_2026-02-21.md`
22
+
9
23
  ## [1.4.1] - 2026-02-21
10
24
 
11
25
  ### Fixed
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  <p align="center">
2
- <img src="./assets/rhythmguard-banner.svg" width="100%" alt="Rhythmguard banner in Geist Pixel" />
2
+ <img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-banner.svg?v=3" width="100%" alt="Rhythmguard banner showing spacing scale ruler and lint output" />
3
3
  </p>
4
4
 
5
5
  # stylelint-plugin-rhythmguard
6
6
 
7
- High-precision spacing governance for CSS and design systems.
7
+ Token governance for CSS and Tailwind. Enforce spacing scales, require design tokens, and catch arbitrary values before they ship.
8
8
 
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/petrilahdelma/stylelint-plugin-rhythmguard/ci.yml?branch=main&label=ci)](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/actions/workflows/ci.yml)
10
10
  [![npm version](https://img.shields.io/npm/v/stylelint-plugin-rhythmguard.svg)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
@@ -12,30 +12,53 @@ High-precision spacing governance for CSS and design systems.
12
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-white.svg)](./LICENSE)
13
13
  [![Node](https://img.shields.io/badge/node-%3E%3D18.18-black.svg)](https://nodejs.org/)
14
14
 
15
- `stylelint-plugin-rhythmguard` enforces scale and token discipline across spacing, radius, typography, size, and translate motion offsets.
15
+ Rhythmguard enforces scale and token discipline across spacing, radius, typography, size, and motion offsets — in CSS declarations and Tailwind class strings.
16
16
 
17
- ## Demo
17
+ Built for teams that want:
18
18
 
19
- <p align="center">
20
- <a href="./assets/rhythmguard-campaign-60s.webm">
21
- <img src="./assets/rhythmguard-campaign-60s.gif" width="100%" alt="Rhythmguard 60-second demo" />
22
- </a>
23
- </p>
19
+ - zero random spacing values in production
20
+ - token-first workflows with autofix migration
21
+ - Tailwind arbitrary value governance (`p-[13px]` `p-[12px]`)
22
+ - consistent layout rhythm across components and pages
24
23
 
25
- I built Rhythmguard after 20 years of watching teams ignore spacing scales and ship arbitrary pixel values everywhere.
24
+ ## Quick Start: Next.js + Tailwind
26
25
 
27
- It is built for teams that want:
26
+ ```bash
27
+ npm install --save-dev stylelint stylelint-plugin-rhythmguard
28
+ ```
29
+
30
+ **.stylelintrc.json:**
31
+
32
+ ```json
33
+ {
34
+ "extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
35
+ }
36
+ ```
37
+
38
+ **eslint.config.js** (for Tailwind class-string governance):
39
+
40
+ ```js
41
+ import rhythmguard from 'stylelint-plugin-rhythmguard/eslint';
42
+
43
+ export default [
44
+ {
45
+ plugins: { 'rhythmguard-tailwind': rhythmguard },
46
+ rules: {
47
+ 'rhythmguard-tailwind/tailwind-class-use-scale': [
48
+ 'error',
49
+ { scale: [0, 4, 8, 12, 16, 24, 32] }
50
+ ],
51
+ },
52
+ },
53
+ ];
54
+ ```
28
55
 
29
- - zero random spacing values in production CSS
30
- - consistent numeric scales for radius, typography, and sizing primitives
31
- - token-first spacing workflows
32
- - predictable autofix behavior for large migrations
33
- - consistent layout rhythm across web surfaces
56
+ This gives you spacing governance in both CSS files and JSX/TSX templates.
34
57
 
35
58
  ## Rule Matrix
36
59
 
37
60
  <p align="center">
38
- <img src="./assets/rhythmguard-rules.svg" width="100%" alt="Rhythmguard rule matrix visual" />
61
+ <img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-rules.svg" width="100%" alt="Rhythmguard rule matrix visual" />
39
62
  </p>
40
63
 
41
64
  | Rule | What it does | Autofix |
@@ -44,15 +67,29 @@ It is built for teams that want:
44
67
  | `rhythmguard/prefer-token` | Enforces token usage over raw spacing literals | Yes, with `tokenMap` |
45
68
  | `rhythmguard/no-offscale-transform` | Enforces scale-aligned `translate*` motion offsets | Yes, nearest safe value |
46
69
 
70
+ ## Demo
71
+
72
+ <p align="center">
73
+ <a href="https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/assets/rhythmguard-campaign-60s.webm">
74
+ <img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-campaign-60s.gif" width="100%" alt="Rhythmguard 60-second demo" />
75
+ </a>
76
+ </p>
77
+
78
+ I built Rhythmguard after 20 years of watching teams ignore spacing scales and ship arbitrary pixel values everywhere.
79
+
47
80
  ## Installation
48
81
 
49
82
  ```bash
50
83
  npm install --save-dev stylelint stylelint-plugin-rhythmguard
51
84
  ```
52
85
 
53
- ## Quick Start
86
+ ## Drop-In for Existing Projects (Recommended)
54
87
 
55
- ### Recommended config
88
+ If your project already uses Stylelint, you only need one command and one config block:
89
+
90
+ ```bash
91
+ npm install --save-dev stylelint-plugin-rhythmguard
92
+ ```
56
93
 
57
94
  ```json
58
95
  {
@@ -60,24 +97,34 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
60
97
  }
61
98
  ```
62
99
 
63
- ### Strict config
100
+ ## Quick Start
101
+
102
+ ### Tailwind config
64
103
 
65
104
  ```json
66
105
  {
67
- "extends": ["stylelint-plugin-rhythmguard/configs/strict"]
106
+ "extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
68
107
  }
69
108
  ```
70
109
 
71
- `strict` intentionally delegates transform translation enforcement to `rhythmguard/no-offscale-transform` to reduce overlapping warnings from `use-scale`.
110
+ ### Recommended config
72
111
 
73
- ### Tailwind config
112
+ ```json
113
+ {
114
+ "extends": ["stylelint-plugin-rhythmguard/configs/recommended"]
115
+ }
116
+ ```
117
+
118
+ ### Strict config
74
119
 
75
120
  ```json
76
121
  {
77
- "extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
122
+ "extends": ["stylelint-plugin-rhythmguard/configs/strict"]
78
123
  }
79
124
  ```
80
125
 
126
+ `strict` intentionally delegates transform translation enforcement to `rhythmguard/no-offscale-transform` to reduce overlapping warnings from `use-scale`.
127
+
81
128
  ### Expanded config
82
129
 
83
130
  ```json
@@ -108,15 +155,34 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
108
155
 
109
156
  `migration` keeps on-scale numeric values temporarily while auto-building token mappings from CSS custom properties and optional Tailwind spacing config.
110
157
 
158
+ ### React / Next.js + Tailwind config
159
+
160
+ ```json
161
+ {
162
+ "extends": ["stylelint-plugin-rhythmguard/configs/react-tailwind"]
163
+ }
164
+ ```
165
+
166
+ `react-tailwind` extends the tailwind config with CSS Modules overrides (spacing + radius enforcement) and ignores Next.js build directories.
167
+
111
168
  Stable shared config entry points:
112
169
 
113
170
  - `stylelint-plugin-rhythmguard/configs/recommended`
114
171
  - `stylelint-plugin-rhythmguard/configs/strict`
115
172
  - `stylelint-plugin-rhythmguard/configs/tailwind`
173
+ - `stylelint-plugin-rhythmguard/configs/react-tailwind`
116
174
  - `stylelint-plugin-rhythmguard/configs/expanded`
117
175
  - `stylelint-plugin-rhythmguard/configs/logical`
118
176
  - `stylelint-plugin-rhythmguard/configs/migration`
119
177
 
178
+ Framework-specific setup for Vue, Lit, Astro, and SvelteKit: [`docs/FRAMEWORKS.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/FRAMEWORKS.md)
179
+
180
+ ## Comparison and Migration Recipes
181
+
182
+ - Side-by-side tool fit guide with migration snippets: [`docs/COMPARISON.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/COMPARISON.md)
183
+ - Real-world before/after excerpts from public repos: [`docs/ADOPTION_DIFFS.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/ADOPTION_DIFFS.md)
184
+ - Distribution submissions to Stylelint discovery surfaces: [`docs/DISTRIBUTION.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/DISTRIBUTION.md)
185
+
120
186
  ### Full custom setup
121
187
 
122
188
  ```json
@@ -389,7 +455,7 @@ Options:
389
455
  | `mathFunctionArguments` | `Record<mathFn, number[]>` | `{}` | Restricts linting to specific 1-based argument indexes per math function |
390
456
  | `ignoreMathFunctionArguments` | `Record<mathFn, number[]>` | `{}` | Excludes specific 1-based argument indexes per math function |
391
457
  | `tokenMap` | `Record<string,string>` | `{}` | Enables autofix from raw value to token |
392
- | `tokenMapFile` | `string` | `null` | JSON file path to merge additional token mappings |
458
+ | `tokenMapFile` | `string` | `null` | JSON file path to merge additional token mappings (supports flat, Style Dictionary, and W3C DTCG formats) |
393
459
  | `tokenMapFromCssCustomProperties` | `boolean` | `false` | Auto-builds mappings from matching custom property declarations in the same stylesheet |
394
460
  | `tokenMapFromTailwindSpacing` | `boolean` | `false` | Auto-builds mappings from `theme.spacing` and `theme.extend.spacing` in Tailwind config |
395
461
  | `tailwindConfigPath` | `string` | `null` | Path to Tailwind config used by `tokenMapFromTailwindSpacing` (`.js`, `.cjs`, `.mjs`) |
@@ -430,6 +496,12 @@ Rhythmguard works well in Tailwind projects, but it enforces what Stylelint can
430
496
  - CSS Modules (for example `*.module.css`)
431
497
  - declarations inside `@layer` blocks
432
498
 
499
+ ### Tailwind v4 @theme tokens
500
+
501
+ The `tailwind` config preset automatically extracts spacing tokens from Tailwind v4 `@theme` blocks and uses them for `prefer-token` enforcement. Raw values like `padding: 16px` are autofixed to `padding: var(--spacing-4)`.
502
+
503
+ See [`docs/TAILWIND.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/TAILWIND.md) for full setup.
504
+
433
505
  ### What Rhythmguard does not cover
434
506
 
435
507
  - Tailwind class strings in templates/JSX/TSX, for example:
@@ -460,6 +532,18 @@ export default [
460
532
 
461
533
  This rule targets arbitrary spacing utilities such as `p-[13px]`, `gap-[18px]`, `translate-x-[10px]`, and autofixes to the nearest configured scale value.
462
534
 
535
+ #### Supported patterns
536
+
537
+ The rule checks every string literal in your code, so it works automatically with common utility functions:
538
+
539
+ - `cn("p-[13px]")` / `cn("p-[13px]", condition && "m-[7px]")`
540
+ - `clsx("p-[13px]", "gap-[18px]")`
541
+ - `twMerge("p-[13px]", otherClasses)`
542
+ - `cva("base", { variants: { size: { sm: "p-[5px]" } } })`
543
+ - `<div className={cn("p-[13px]")} />`
544
+
545
+ No extra config needed — if the string contains an arbitrary spacing value, it gets caught and autofixed.
546
+
463
547
  ### Recommended stack for full Tailwind enforcement
464
548
 
465
549
  Use both layers:
@@ -501,6 +585,35 @@ console.log(rhythmguard.presets.scales['rhythmic-4']);
501
585
  console.log(Object.keys(rhythmguard.eslint.rules));
502
586
  ```
503
587
 
588
+ ## Token File Formats
589
+
590
+ The `tokenMapFile` option supports multiple JSON formats:
591
+
592
+ **Flat token-to-value:**
593
+
594
+ ```json
595
+ { "--spacing-4": "16px", "--spacing-3": "12px" }
596
+ ```
597
+
598
+ **Style Dictionary:**
599
+
600
+ ```json
601
+ { "--spacing-4": { "value": "16px" } }
602
+ ```
603
+
604
+ **W3C DTCG (Design Token Community Group):**
605
+
606
+ ```json
607
+ {
608
+ "spacing": {
609
+ "4": { "$value": "16px", "$type": "dimension" },
610
+ "2": { "$value": "8px", "$type": "dimension" }
611
+ }
612
+ }
613
+ ```
614
+
615
+ Nested DTCG groups are walked recursively. The key path becomes the CSS variable name: `spacing.4` → `var(--spacing-4)`. Non-length values (colors, fonts) are ignored automatically.
616
+
504
617
  ## Autofix Philosophy
505
618
 
506
619
  Rhythmguard only applies deterministic fixes:
@@ -545,6 +658,21 @@ Detailed methodology and custom args are documented in [`docs/BENCHMARKING.md`](
545
658
  ## Article
546
659
 
547
660
  - Dev.to: [Enforcing your spacing standards with Rhythmguard](https://dev.to/petrilahdelma/enforcing-your-spacing-standards-with-rhythmguard-a-custom-stylelint-plugin-1ojj)
661
+ - Original article update note (Feb 21, 2026): [`docs/DEVTO_ORIGINAL_UPDATE_NOTE_2026-02-21.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/DEVTO_ORIGINAL_UPDATE_NOTE_2026-02-21.md)
662
+ - Continuation draft (ready to publish): [`docs/DEVTO_CONTINUATION_2026-02-21.md`](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/blob/main/docs/DEVTO_CONTINUATION_2026-02-21.md)
663
+
664
+ ## Used by and Community Examples
665
+
666
+ Public codebases currently used for production migration examples:
667
+
668
+ - [PetriLahdelma/digitaltableteur-nextjs](https://github.com/PetriLahdelma/digitaltableteur-nextjs)
669
+ - [PetriLahdelma/digitaltableteur](https://github.com/PetriLahdelma/digitaltableteur)
670
+
671
+ Want your team listed here?
672
+
673
+ 1. Open an issue with `used-by` in the title.
674
+ 2. Include one before/after diff and your Rhythmguard config.
675
+ 3. Add migration notes (false positives, rules enabled, rollout phase).
548
676
 
549
677
  ## Release Workflow
550
678
 
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "1.4.1",
4
- "description": "Stylelint plugin for spacing scale, token enforcement, and Tailwind class-string governance",
3
+ "version": "1.5.0",
4
+ "description": "Token governance for CSS and Tailwind — enforce spacing scales, require design tokens, catch arbitrary values",
5
+ "bin": {
6
+ "rhythmguard": "./src/cli/index.js"
7
+ },
5
8
  "keywords": [
6
9
  "stylelint",
7
10
  "stylelint-plugin",
@@ -46,6 +49,10 @@
46
49
  "require": "./src/configs/migration.js",
47
50
  "import": "./src/configs/migration.mjs"
48
51
  },
52
+ "./configs/react-tailwind": {
53
+ "require": "./src/configs/react-tailwind.js",
54
+ "import": "./src/configs/react-tailwind.mjs"
55
+ },
49
56
  "./presets": {
50
57
  "require": "./src/presets/index.js",
51
58
  "import": "./src/presets/index.mjs"
@@ -68,7 +75,6 @@
68
75
  }
69
76
  },
70
77
  "files": [
71
- "assets",
72
78
  "scales",
73
79
  "schemas",
74
80
  "src",
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const args = process.argv.slice(3);
7
+ const jsonMode = args.includes('--json');
8
+ const dir = args.find((a) => !a.startsWith('-'));
9
+
10
+ if (!dir) {
11
+ process.stderr.write('Usage: rhythmguard audit <dir> [--json]\n');
12
+ process.exit(1);
13
+ }
14
+
15
+ const resolvedDir = path.resolve(dir);
16
+
17
+ if (!fs.existsSync(resolvedDir)) {
18
+ process.stderr.write(`Directory not found: ${dir}\n`);
19
+ process.exit(1);
20
+ }
21
+
22
+ const GLOB_PATTERN = `${resolvedDir}/**/*.{css,module.css}`;
23
+
24
+ const pluginPath = path.resolve(__dirname, '..', 'index.js');
25
+
26
+ async function run() {
27
+ const { default: stylelint } = await import('stylelint');
28
+
29
+ let result;
30
+ try {
31
+ result = await stylelint.lint({
32
+ files: GLOB_PATTERN,
33
+ config: {
34
+ plugins: [pluginPath],
35
+ rules: {
36
+ 'rhythmguard/use-scale': [true, { severity: 'warning' }],
37
+ 'rhythmguard/prefer-token': [
38
+ true,
39
+ {
40
+ tokenMapFromCssCustomProperties: true,
41
+ severity: 'warning',
42
+ },
43
+ ],
44
+ },
45
+ },
46
+ });
47
+ } catch (err) {
48
+ process.stderr.write(`Lint error: ${err.message}\n`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const fileResults = result.results || [];
53
+ const totalFiles = fileResults.length;
54
+
55
+ const offScaleValues = {};
56
+ const tokenOpportunities = {};
57
+ let filesWithIssues = 0;
58
+ let totalWarnings = 0;
59
+
60
+ for (const fileResult of fileResults) {
61
+ const warnings = fileResult.warnings || [];
62
+ if (warnings.length > 0) {
63
+ filesWithIssues++;
64
+ }
65
+
66
+ for (const warning of warnings) {
67
+ totalWarnings++;
68
+ const text = warning.text || '';
69
+
70
+ // Extract off-scale values from use-scale messages
71
+ // Format: Unexpected off-scale value "13px". Use scale values (nearest: 12px or 16px).
72
+ const offScaleMatch = text.match(
73
+ /Unexpected off-scale value "([^"]+)"/,
74
+ );
75
+ if (offScaleMatch) {
76
+ const value = offScaleMatch[1];
77
+ offScaleValues[value] = (offScaleValues[value] || 0) + 1;
78
+ }
79
+
80
+ // Extract token opportunities from prefer-token messages
81
+ // Format: Unexpected raw scale value "16px". Use design tokens for scale decisions.
82
+ const tokenMatch = text.match(
83
+ /Unexpected raw scale value "([^"]+)"/,
84
+ );
85
+ if (tokenMatch) {
86
+ const value = tokenMatch[1];
87
+ tokenOpportunities[value] = (tokenOpportunities[value] || 0) + 1;
88
+ }
89
+ }
90
+ }
91
+
92
+ const sortedOffScale = Object.entries(offScaleValues)
93
+ .sort((a, b) => b[1] - a[1])
94
+ .slice(0, 10);
95
+
96
+ const sortedTokenOps = Object.entries(tokenOpportunities)
97
+ .sort((a, b) => b[1] - a[1])
98
+ .slice(0, 10);
99
+
100
+ if (jsonMode) {
101
+ const output = {
102
+ directory: dir,
103
+ totalFiles,
104
+ filesWithIssues,
105
+ totalWarnings,
106
+ offScaleValues: Object.fromEntries(sortedOffScale),
107
+ tokenOpportunities: Object.fromEntries(sortedTokenOps),
108
+ };
109
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
110
+ return;
111
+ }
112
+
113
+ // Human-readable output
114
+ const pct = totalFiles > 0
115
+ ? Math.round((filesWithIssues / totalFiles) * 100)
116
+ : 0;
117
+
118
+ const lines = [
119
+ '',
120
+ `Rhythmguard Audit: ${dir}`,
121
+ '',
122
+ `Files scanned: ${totalFiles}`,
123
+ `Files with issues: ${filesWithIssues} (${pct}%)`,
124
+ '',
125
+ ];
126
+
127
+ if (sortedOffScale.length > 0) {
128
+ const offScaleTotal = Object.values(offScaleValues).reduce(
129
+ (a, b) => a + b,
130
+ 0,
131
+ );
132
+ lines.push(`Off-scale values: ${offScaleTotal}`);
133
+ for (const [value, count] of sortedOffScale) {
134
+ lines.push(` ${value.padEnd(10)} ×${count}`);
135
+ }
136
+ lines.push('');
137
+ }
138
+
139
+ if (sortedTokenOps.length > 0) {
140
+ const tokenTotal = Object.values(tokenOpportunities).reduce(
141
+ (a, b) => a + b,
142
+ 0,
143
+ );
144
+ lines.push(`Token opportunities: ${tokenTotal}`);
145
+ for (const [value, count] of sortedTokenOps) {
146
+ lines.push(` ${value.padEnd(10)} ×${count}`);
147
+ }
148
+ lines.push('');
149
+ }
150
+
151
+ if (totalWarnings === 0) {
152
+ lines.push('No issues found. Your spacing is on scale.');
153
+ } else {
154
+ lines.push('Run "npx stylelint --fix" to auto-correct.');
155
+ }
156
+
157
+ lines.push('');
158
+ process.stdout.write(lines.join('\n'));
159
+ }
160
+
161
+ run();