stylelint-plugin-rhythmguard 3.10.0 → 3.10.1

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,12 @@ The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.10.1] - 2026-10-15
10
+
11
+ ### Fixed
12
+
13
+ - Token ladders: a ladder whose every step is a `var()` alias of a step on one other ladder, such as Tailwind `@theme` names over a token file, is folded into that ladder instead of being reported again with the same problems. The ladder it aliases lists it under `aliases`, and the Markdown report names it next to that ladder.
14
+
9
15
  ## [3.10.0] - 2026-10-15
10
16
 
11
17
  ### Added
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  Nobody chose 13px. Rhythmguard catches off-scale spacing in CSS and Tailwind class strings, tells you the nearest steps on your scale, and snaps to them or to your tokens when you ask.
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
- [![npm version](https://img.shields.io/npm/v/stylelint-plugin-rhythmguard?label=npm&color=1f6feb&r=3100)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
10
+ [![npm version](https://img.shields.io/npm/v/stylelint-plugin-rhythmguard?label=npm&color=1f6feb&r=3101)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
11
11
  [![npm downloads](https://img.shields.io/npm/dm/stylelint-plugin-rhythmguard.svg)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
12
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-white.svg)](./LICENSE)
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "3.10.0",
3
+ "version": "3.10.1",
4
4
  "description": "Nobody chose 13px. Catches off-scale spacing in CSS and Tailwind class strings and snaps it to your scale or tokens. Stylelint rules, an ESLint companion, and an audit CLI.",
5
5
  "bin": {
6
6
  "rhythmguard": "src/cli/index.js"
@@ -141,7 +141,8 @@ function appendTokenLaddersMarkdown(lines, ladders) {
141
141
  const note = `${LADDER_PROBLEM_TEXT[problem.type]}${where} with \`${problem.tokens[0]}\``;
142
142
  flagged.set(problem.tokens[1], [...(flagged.get(problem.tokens[1]) || []), note]);
143
143
  }
144
- lines.push(`**\`${escapeMarkdown(entry.prefix)}\`**`);
144
+ const aliasNote = entry.aliases ? ` (also aliased as ${entry.aliases.map((alias) => `\`${escapeMarkdown(alias)}\``).join(', ')})` : '';
145
+ lines.push(`**\`${escapeMarkdown(entry.prefix)}\`**${aliasNote}`);
145
146
  lines.push('');
146
147
  lines.push('| Token | Min | Max | Problem |');
147
148
  lines.push('| --- | --- | --- | --- |');
@@ -19,7 +19,8 @@ const { formatLength, numbersEqual, parseLengthToken, toPx } = require('../core/
19
19
  * Each problem names the end it occurs at: `min`, `max`, or `both`.
20
20
  *
21
21
  * A step that cannot be evaluated (calc(), a viewport length, a token with
22
- * several different definitions) is left out of the comparison.
22
+ * several different definitions) is left out of the comparison. A ladder
23
+ * that only aliases another one step for step is folded into it.
23
24
  */
24
25
  const DEFAULT_MIN_STEP_PX = 2;
25
26
  const MIN_LADDER_STEPS = 3;
@@ -158,9 +159,37 @@ function resolveTokenLadders({ baseFontSize = 16, declarations, minStepPx = DEFA
158
159
  ladder.steps.push({ max: range.max, min: range.min, rank: step.rank, token });
159
160
  }
160
161
 
162
+ const eligible = [...ladders.values()].filter((ladder) => ladder.steps.length >= MIN_LADDER_STEPS);
163
+ const ladderOfToken = new Map();
164
+ for (const ladder of eligible) {
165
+ for (const step of ladder.steps) ladderOfToken.set(step.token, ladder);
166
+ }
167
+ // A ladder whose every step is a var() alias of a step on one other ladder
168
+ // (Tailwind @theme names over a token file) has that ladder's problems, not
169
+ // its own. It is listed under the other ladder's `aliases` and not counted.
170
+ const aliasTarget = (ladder) => {
171
+ let target = null;
172
+ for (const step of ladder.steps) {
173
+ const values = declarations.get(step.token);
174
+ const reference = values && values.size === 1 ? [...values][0].trim().match(VAR_REFERENCE) : null;
175
+ const owner = reference ? ladderOfToken.get(reference[1]) : null;
176
+ if (!owner || owner === ladder || (target && owner !== target)) return null;
177
+ target = owner;
178
+ }
179
+ return target;
180
+ };
181
+ const aliases = new Map();
182
+ for (const ladder of eligible) {
183
+ const target = aliasTarget(ladder);
184
+ if (target && !aliasTarget(target)) {
185
+ aliases.set(ladder, target);
186
+ target.aliases = [...(target.aliases || []), ladder.prefix].sort();
187
+ }
188
+ }
189
+
161
190
  const entries = [];
162
- for (const ladder of ladders.values()) {
163
- if (ladder.steps.length < MIN_LADDER_STEPS) continue;
191
+ for (const ladder of eligible) {
192
+ if (aliases.has(ladder)) continue;
164
193
  const steps = ladder.steps.sort((a, b) => a.rank - b.rank);
165
194
  const problems = [];
166
195
  for (let index = 0; index < steps.length - 1; index += 1) {
@@ -176,6 +205,7 @@ function resolveTokenLadders({ baseFontSize = 16, declarations, minStepPx = DEFA
176
205
  if (atMax) problems.push({ end: 'max', tokens, ...atMax });
177
206
  }
178
207
  entries.push({
208
+ ...(ladder.aliases ? { aliases: ladder.aliases } : {}),
179
209
  prefix: ladder.prefix,
180
210
  problems,
181
211
  steps: steps.map(({ max, min, token }) => ({