stylelint-plugin-rhythmguard 2.2.0 → 3.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,45 @@ The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.1.0] - 2026-09-06
10
+
11
+ ### Added
12
+
13
+ - Audit findings for CSS declarations carry the declaration's `property`, recovered from the source at the warning position (`null` inside at-rules). Off-scale findings are counted by property in `offScaleProperties`, exposed as `contracts.scale.offScaleProperties`, rendered as a histogram in text, Markdown and HTML output and as a `Top properties` line in the quickstart. Baseline keys and benchmark snapshots are unchanged. ([#64](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/64))
14
+
15
+ ### Changed
16
+
17
+ - `rhythmguard audit --help` describes what `--scale auto` reads and in which order, SCSS scanning through `postcss-scss`, and the value, property and file breakdowns.
18
+ - CI and release workflows no longer run the full suite on the Stylelint 16.0.0 floor as an allowed-to-fail step; that step produced an error annotation on every green run. The blocking floor suite is unchanged. Actions `checkout` and `setup-node` moved to v5.
19
+
20
+ ## [3.0.0] - 2026-09-06
21
+
22
+ Upgrade notes: [`docs/MIGRATING_TO_3.md`](./docs/MIGRATING_TO_3.md).
23
+
24
+ ### Breaking
25
+
26
+ - Node 20.19 or newer is required. Node 18 support is removed.
27
+ - `stylelint-config-tailwindcss` and `stylelint-plugin-logical-css` are optional peer dependencies instead of dependencies. Install `stylelint-config-tailwindcss` yourself if you extend `configs/tailwind`. The package now has one runtime dependency, `known-css-properties`.
28
+ - Removed configs `expanded`, `logical`, `migration` and `react-tailwind`. Their rule blocks are reproduced in the migration guide. `plugin.configs` has five keys: `embed`, `motion`, `recommended`, `strict`, `tailwind`.
29
+ - `rhythmguard init` writes the `tailwind` config plus Next.js build ignores for Next.js projects instead of `react-tailwind`.
30
+
31
+ ### Removed
32
+
33
+ - Campaign media, slide images, outreach drafts and old planning documents are no longer in the repository. The README banner and the cover image referenced by a published article remain.
34
+
35
+ ### Added
36
+
37
+ - The audit now scans `.scss` files through `postcss-scss` (new optional peer dependency). Without it, SCSS files are reported as skipped instead of ignored. `scanned.scssFiles` and `scanned.scssSkipped` join the JSON contract; text and Markdown output show the counts.
38
+ - Quiet benchmark widened from 7 to 20 public repositories, adding SCSS-first systems: Bootstrap, Primer CSS, Penpot, Mastodon, Gutenberg components, GitLab UI, Pico, Bulma, Spectrum CSS, USWDS, Carbon, Salesforce Lightning, wp-calypso components.
39
+
40
+ - `scale: "auto"` now reads spacing tokens shipped by installed design-token packages when the project's own stylesheets define none: `tailwindcss` (v4 `theme.css`), `@radix-ui/themes`, `@mantine/core`, `@primer/primitives`, `@shopify/polaris-tokens`, `@spectrum-css/tokens`. Provenance is `token-package`. Token-source entries accept a per-file `tokenPattern` for packages that name spacing differently. The allowlist is `src/utils/token-packages.json`.
41
+ - Sass variables and maps are token sources for `--scale auto`, for `scaleSources` files, and for `scale: "auto"` inside the rules when the linted `.scss` file declares them (with `postcss-scss`): `$spacer: 1rem`, `$spacing-01: 0.125rem`, and maps such as `$spacers: (1: $spacer * .25, ...)` with nested maps, variable references, `* / + -` arithmetic and `math.div()`. Unevaluable function calls, strings, keywords and interpolated keys are skipped. Bootstrap, Bulma, Carbon and USWDS now infer their own scales in the quiet benchmark.
42
+
43
+ ### Changed
44
+
45
+ - New README banner.
46
+ - Contributing: rewritten `CONTRIBUTING.md` with ways to contribute and the local gate, a Contributor Covenant code of conduct, a false-positive issue template, a current security policy, and a GitHub-hosted CI job so pull requests from forks get lint, typecheck and test feedback.
47
+
9
48
  ## [2.2.0] - 2026-09-05
10
49
 
11
50
  ### Added
package/CONTRIBUTING.md CHANGED
@@ -1,84 +1,99 @@
1
- # Contributing
1
+ # Contributing to Rhythmguard
2
2
 
3
- Thanks for contributing to `stylelint-plugin-rhythmguard`.
3
+ Thank you for being here. Rhythmguard is a small, opinionated tool with one job: keep spacing on a scale. Most of the useful work on it is small too, and a first contribution can land in an afternoon. This page tells you where the useful work is, how the checks run, and what to expect from review.
4
4
 
5
- ## Development Setup
5
+ ## Ways to contribute
6
6
 
7
- ```bash
8
- npm ci
9
- npm run lint
10
- npm test
11
- ```
7
+ **Report a false positive.** A finding your team considers wrong is the most valuable report we get. It is how percentages and hairlines stopped being findings. Open a [false-positive report](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/issues/new?template=false-positive.yml) with the CSS or class string and your config. No fix required.
8
+
9
+ **Add a repository to the quiet benchmark.** The benchmark audits public design systems and fails CI when the findings change. Adding a repo is one JSON entry in `benchmarks/quiet/repos.json` plus `npm run bench:quiet -- --only <name>` to create its snapshot. Systems with tokens in unusual places are the most useful additions.
10
+
11
+ **Review a drift list.** Each benchmark repo has a list of findings that heuristics call real drift. Labelling them as real or as an allowance in `benchmarks/quiet/labels/<repo>.json` with a one-line reason turns a heuristic number into a reviewed one, and any systematic pattern you find becomes a candidate default.
12
+
13
+ **Teach inference a new token source.** Sass variables and maps, token packages, framework theme files. If your scale lives somewhere the tool cannot see, the fix is a parser in `src/utils/token-sources.js` and a test.
12
14
 
13
- Optional checks:
15
+ **Improve a rule page.** Every rule has a page under `docs/rules/`. A clearer example or a missing edge case is a welcome PR.
16
+
17
+ **Contribute a community scale.** JSON files in `scales/community/`, scaffolded with `npm run scales:add -- --name my-team-scale --base 8 --steps 0,4,8,12,16,24,32` and checked with `npm run scales:validate`. Spec and policy in [`docs/COMMUNITY_SCALES.md`](./docs/COMMUNITY_SCALES.md).
18
+
19
+ Issues labelled [`good first issue`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/labels/good%20first%20issue) are scoped for a first PR. [`help wanted`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/labels/help%20wanted) marks work the maintainer would like a second pair of hands on. If you want to work on something larger, open an issue first so we can agree the shape before you write code.
20
+
21
+ ## Getting set up
14
22
 
15
23
  ```bash
16
- npm run test:compat-floor
17
- npm run test:coverage
18
- npm run test:pack-smoke
19
- npm run scales:validate
24
+ git clone https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard.git
25
+ cd stylelint-plugin-rhythmguard
26
+ npm ci
27
+ npm test
20
28
  ```
21
29
 
22
- ## Community Scale Contributions
30
+ Node 20.19 or newer. No build step; the source runs as is. Tests use `node --test` and take about five seconds.
23
31
 
24
- Rhythmguard accepts community presets through JSON files in `scales/community`.
32
+ ## The local gate
25
33
 
26
- Create a new scale file:
34
+ Run these before you push. They are what CI runs, so a green local run means a green PR.
27
35
 
28
36
  ```bash
29
- npm run scales:add -- --name my-team-scale --base 8 --steps 0,4,8,12,16,24,32
37
+ npm run lint # eslint
38
+ npm run typecheck # the published TypeScript declarations against a consumer file and the examples
39
+ npm test # 150+ tests, Stylelint 16
30
40
  ```
31
41
 
32
- Then validate:
42
+ Optional, depending on what you touched:
33
43
 
34
44
  ```bash
35
- npm run scales:validate
45
+ npm run test:compat-floor # Stylelint 16.0.0, the oldest supported
46
+ npm run test:pack-smoke # pack the tarball and install it in a temp project
47
+ npm run scales:validate # community scale JSON
48
+ npm run bench:quiet -- --check # findings on the benchmark repos must match snapshots
36
49
  ```
37
50
 
38
- Scale files must pass schema and collision checks. See [`docs/COMMUNITY_SCALES.md`](./docs/COMMUNITY_SCALES.md) for the full spec and policy.
51
+ If your shell's npm registry is overridden by a corporate `.npmrc`, add `--registry https://registry.npmjs.org` to `npm ci` and prefix `test:pack-smoke` with `npm_config_registry=https://registry.npmjs.org`.
39
52
 
40
- ## Compatibility Targets
53
+ ### Pull requests from forks
41
54
 
42
- - Stylelint `^16.0.0 || ^17.0.0`. Stylelint `16.0.0` has known autofix and API differences; CI runs the floor-compat suite against it and the full suite non-blocking.
43
- - Node `>=18.18.0` for Stylelint 16, `>=20.19.0` for Stylelint 17. The CI matrix excludes Node 18 with Stylelint 17.
44
- - Dual CommonJS and ESM entry points; every export has a declaration under `types/`. `npm run typecheck` compiles the declarations, a consumer check file, and the examples.
55
+ CI for this repository runs on self-hosted runners for pushes and same-repo branches. For pull requests from forks a separate job runs on GitHub-hosted runners with Node 22 and current Stylelint 16, covering lint, typecheck and the test suite. You will see its result on your PR. The full matrix and the benchmark run after merge.
45
56
 
46
- ## Performance Benchmarking
57
+ ## How we write changes
47
58
 
48
- ```bash
49
- npm run bench:perf
50
- npm run bench:perf:fix
51
- ```
59
+ **Tests first.** Every behaviour change starts with a failing test. If you send a fix without one, expect the review to ask for it; the test is what proves the fix is the right one and keeps it that way.
60
+
61
+ **Rules explain their defaults.** A new default, or a change to one, needs a written reason on the rule's page under `docs/rules/` and, where the benchmark can show it, a number. The hairline section in [`docs/rules/use-scale.md`](./docs/rules/use-scale.md#hairlines) is the model.
62
+
63
+ **The benchmark is the regression suite for real code.** If your change alters findings on any benchmark repository, `npm run bench:quiet -- --check` fails and prints the diff. Review it. If the new findings are right, run `npm run bench:quiet -- --update-snapshots`, commit `benchmarks/quiet/snapshots/`, and say in the PR why they are right.
64
+
65
+ **Fixes are deterministic.** Autofix snaps to the nearest scale step or to an explicit token map. It never guesses a token. Keep that property.
52
66
 
53
- Compares runtime against `stylelint-scales` on a deterministic corpus. Method and arguments: [`docs/BENCHMARKING.md`](./docs/BENCHMARKING.md).
67
+ **Prose without em dashes.** House style, applied to docs and messages alike.
54
68
 
55
- ## Semver Rules
69
+ ## Semver
56
70
 
57
- - Patch (`x.y.Z`): bug fixes, docs updates, non-breaking internal changes.
58
- - Minor (`x.Y.z`): backward-compatible new options, presets, or behavior.
59
- - Major (`X.y.z`): any breaking behavior change for existing rules/configs.
71
+ - Patch: bug fixes, docs, internal changes.
72
+ - Minor: new options, presets, sources, or behaviour that does not change existing reports.
73
+ - Major: any change to default reports for existing configs, to autofix behaviour, or to exported entry points.
60
74
 
61
- Breaking examples:
75
+ `configs/recommended`, `configs/strict`, `configs/tailwind` and `configs/embed` stay stable within a major. The `embed` config's shape is frozen for 2.x because shared configs depend on it.
62
76
 
63
- - changing default scale behavior
64
- - changing autofix behavior in a non-compatible way
65
- - changing/removing exported config entry points
77
+ ## Compatibility
66
78
 
67
- ## Rule Change Requirements
79
+ - Stylelint `^16.0.0 || ^17.0.0`. The 16.0.0 floor has known autofix differences; CI runs the floor suite against it.
80
+ - Node `>=20.19.0`.
81
+ - CommonJS and ESM entry points; every export has a declaration under `types/`.
82
+ - One runtime dependency, `known-css-properties`. `postcss-scss`, `stylelint-config-tailwindcss` and `stylelint-plugin-logical-css` are optional peers and dev dependencies here.
68
83
 
69
- When changing rule logic:
84
+ ## Review
70
85
 
71
- 1. add/adjust tests for the behavior
72
- 2. validate deterministic fix behavior
73
- 3. update the rule's page in `docs/rules/` if options or behavior changed
74
- 4. update CHANGELOG
75
- 5. run the quiet benchmark: `npm run bench:quiet -- --check`. It audits real public design systems at pinned commits and fails when the finding set or the inferred scale changes. If the change is intended, review the printed diff, run `npm run bench:quiet -- --update-snapshots`, and commit `benchmarks/quiet/snapshots/` with a sentence in the PR on why the new findings are right. CI runs the same check.
86
+ The maintainer aims to respond to issues and PRs within a week. Small PRs merge faster than large ones; if a change can be split, split it. Reviews are about the change, never the person, and the [code of conduct](./CODE_OF_CONDUCT.md) applies everywhere in this project.
76
87
 
77
- ## Release Workflow
88
+ ## Releases
78
89
 
79
- 1. Run the local gate: `npm run lint && npm run typecheck && npm test && npm run test:pack-smoke`.
80
- 2. Update `CHANGELOG.md` and bump `package.json`.
81
- 3. Create a GitHub release. `release.yml` runs the Node and Stylelint matrix on the self-hosted runners, then publishes to npm with provenance when `NPM_TOKEN` is configured, or skips publish with a notice when it is not.
82
- 4. `post-publish-smoke.yml` installs the published version in a clean project.
90
+ Maintainer-run. Bump `package.json` and `CHANGELOG.md`, create a GitHub release, and `release.yml` verifies on the self-hosted matrix and publishes through npm trusted publishing (OIDC) from a GitHub-hosted job. Provenance is automatic. The checklist is in [`docs/RELEASE_CHECKLIST.md`](./docs/RELEASE_CHECKLIST.md).
91
+
92
+ ## Benchmarking performance
93
+
94
+ ```bash
95
+ npm run bench:perf
96
+ npm run bench:perf:fix
97
+ ```
83
98
 
84
- Keep `configs/recommended` and `configs/strict` stable across minors. Rule docs live in `docs/rules/`; a test checks that every rule's `meta.url` points at its page.
99
+ Compares runtime against `stylelint-scales` on a deterministic corpus. Method in [`docs/BENCHMARKING.md`](./docs/BENCHMARKING.md).
package/README.md CHANGED
@@ -1,19 +1,19 @@
1
1
  <p align="center">
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" />
2
+ <img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-banner.svg?v=5" 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
- Spacing scale and design-token governance for CSS and Tailwind. `padding: 13px` and `p-[13px]` get reported with the nearest on-scale values, and fixed to them when you ask.
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.svg)](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
10
+ [![npm version](https://img.shields.io/npm/v/stylelint-plugin-rhythmguard?label=npm&color=1f6feb)](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
 
14
14
  Rhythmguard is scale-aware rather than a blanket ban: values on your scale pass, values off it are reported with the two nearest steps, and tokens are only ever suggested from a map you control. It works on CSS declarations through Stylelint and on Tailwind class strings through an ESLint companion, and it ships an audit CLI so you can measure drift and ratchet it down before enforcing anything.
15
15
 
16
- What it is not: it does not check colors or hex values, the Stylelint rules do not see Tailwind class strings (that is the separate ESLint companion below), and it does not parse SCSS in the audit. Pair it with a color linter if you need one; do not expect one tool to do both.
16
+ What it is not: it does not check colors or hex values, and the Stylelint rules do not see Tailwind class strings (that is the separate ESLint companion below). Pair it with a color linter if you need one; do not expect one tool to do both. SCSS is audited when `postcss-scss` is installed.
17
17
 
18
18
  ## Start here
19
19
 
@@ -70,7 +70,7 @@ Every rule validates its options up front. Unknown option names and wrong shapes
70
70
 
71
71
  ## Configs
72
72
 
73
- `recommended`, `strict`, `tailwind`, `react-tailwind`, `expanded`, `logical`, `migration`, `motion`, and `embed` for authors of shared configs (see [docs/FOR_CONFIG_AUTHORS.md](docs/FOR_CONFIG_AUTHORS.md)). All are `stylelint-plugin-rhythmguard/configs/<name>`. What each enables, the full custom setup, and the scale-selection precedence are in [docs/CONFIGS.md](docs/CONFIGS.md). Built-in and community scale presets are in [docs/SCALE_PRESETS.md](docs/SCALE_PRESETS.md).
73
+ `recommended`, `strict`, `tailwind`, `motion` (experimental), and `embed` for authors of shared configs (see [docs/FOR_CONFIG_AUTHORS.md](docs/FOR_CONFIG_AUTHORS.md)). All are `stylelint-plugin-rhythmguard/configs/<name>`. What each enables, the full custom setup, and the scale-selection precedence are in [docs/CONFIGS.md](docs/CONFIGS.md). Built-in and community scale presets are in [docs/SCALE_PRESETS.md](docs/SCALE_PRESETS.md).
74
74
 
75
75
  ## Audit before you enforce
76
76
 
@@ -99,7 +99,7 @@ The audit scans CSS declarations, Tailwind class strings and your token contract
99
99
 
100
100
  ## Compatibility
101
101
 
102
- Stylelint 16 and 17. Node 18.18 or newer for Stylelint 16, Node 20.19 or newer for Stylelint 17. CommonJS and ESM entry points, TypeScript declarations for every export. The CI matrix runs Node 18, 20 and 22 against Stylelint 16.0.0, 16.x and 17.x.
102
+ Stylelint 16 and 17. Node 20.19 or newer. One runtime dependency (`known-css-properties`); `postcss-scss`, `stylelint-config-tailwindcss` and `stylelint-plugin-logical-css` are optional peers. CommonJS and ESM entry points, TypeScript declarations for every export. The CI matrix runs Node 20 and 22 against Stylelint 16.0.0, 16.x and 17.x. Upgrading from 2.x: [docs/MIGRATING_TO_3.md](docs/MIGRATING_TO_3.md).
103
103
 
104
104
  ## Contributing and support
105
105
 
package/SECURITY.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | --- | --- |
7
- | 0.1.x | Yes |
8
- | < 0.1.0 | No |
7
+ | 2.x | Yes |
8
+ | < 2.0 | No, upgrade to 2.x |
9
9
 
10
10
  ## Reporting a Vulnerability
11
11
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stylelint-plugin-rhythmguard",
3
- "version": "2.2.0",
4
- "description": "Token governance for CSS and Tailwind enforce spacing scales, require design tokens, catch arbitrary values",
3
+ "version": "3.1.0",
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"
7
7
  },
@@ -47,31 +47,11 @@
47
47
  "require": "./src/configs/tailwind.js",
48
48
  "import": "./src/configs/tailwind.mjs"
49
49
  },
50
- "./configs/expanded": {
51
- "types": "./types/config.d.ts",
52
- "require": "./src/configs/expanded.js",
53
- "import": "./src/configs/expanded.mjs"
54
- },
55
- "./configs/logical": {
56
- "types": "./types/config.d.ts",
57
- "require": "./src/configs/logical.js",
58
- "import": "./src/configs/logical.mjs"
59
- },
60
- "./configs/migration": {
61
- "types": "./types/config.d.ts",
62
- "require": "./src/configs/migration.js",
63
- "import": "./src/configs/migration.mjs"
64
- },
65
50
  "./configs/motion": {
66
51
  "types": "./types/config.d.ts",
67
52
  "require": "./src/configs/motion.js",
68
53
  "import": "./src/configs/motion.mjs"
69
54
  },
70
- "./configs/react-tailwind": {
71
- "types": "./types/config.d.ts",
72
- "require": "./src/configs/react-tailwind.js",
73
- "import": "./src/configs/react-tailwind.mjs"
74
- },
75
55
  "./presets": {
76
56
  "types": "./types/presets.d.ts",
77
57
  "require": "./src/presets/index.js",
@@ -145,27 +125,41 @@
145
125
  "email": "hello@petrilahdelma.com"
146
126
  },
147
127
  "peerDependencies": {
148
- "stylelint": "^16.0.0 || ^17.0.0"
149
- },
150
- "dependencies": {
151
- "known-css-properties": "^0.37.0",
128
+ "stylelint": "^16.0.0 || ^17.0.0",
129
+ "postcss-scss": "^4.0.0",
152
130
  "stylelint-config-tailwindcss": "^1.0.1",
153
131
  "stylelint-plugin-logical-css": "^2.0.2"
154
132
  },
133
+ "peerDependenciesMeta": {
134
+ "postcss-scss": {
135
+ "optional": true
136
+ },
137
+ "stylelint-config-tailwindcss": {
138
+ "optional": true
139
+ },
140
+ "stylelint-plugin-logical-css": {
141
+ "optional": true
142
+ }
143
+ },
144
+ "dependencies": {
145
+ "known-css-properties": "^0.37.0"
146
+ },
155
147
  "devDependencies": {
156
148
  "@eslint/js": "^9.22.0",
157
149
  "@types/node": "^22.20.1",
158
150
  "c8": "^10.1.3",
159
151
  "eslint": "^9.22.0",
160
152
  "geist": "^1.7.0",
153
+ "postcss-scss": "^4.0.9",
161
154
  "postcss-value-parser": "^4.2.0",
162
155
  "stylelint": "^16.15.0",
156
+ "stylelint-config-tailwindcss": "^1.0.1",
163
157
  "stylelint-plugin-logical-css": "^2.0.2",
164
158
  "stylelint-scales": "^5.0.0",
165
159
  "typescript": "^5.9.3"
166
160
  },
167
161
  "engines": {
168
- "node": ">=18.18.0"
162
+ "node": ">=20.19.0"
169
163
  },
170
164
  "publishConfig": {
171
165
  "access": "public"
@@ -1,10 +1,33 @@
1
1
  {
2
2
  "name": "product-decimal-10",
3
- "description": "Decimal-friendly product spacing scale for dashboard-heavy enterprise UIs.",
3
+ "description": "Decimal-friendly product spacing scale for dashboard-heavy UIs.",
4
4
  "base": 10,
5
- "steps": [0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64, 80],
6
- "aliases": ["decimal-10", "enterprise-10"],
7
- "tags": ["product", "dashboard", "community"],
5
+ "steps": [
6
+ 0,
7
+ 2,
8
+ 4,
9
+ 6,
10
+ 8,
11
+ 10,
12
+ 12,
13
+ 16,
14
+ 20,
15
+ 24,
16
+ 32,
17
+ 40,
18
+ 48,
19
+ 64,
20
+ 80
21
+ ],
22
+ "aliases": [
23
+ "decimal-10",
24
+ "enterprise-10"
25
+ ],
26
+ "tags": [
27
+ "product",
28
+ "dashboard",
29
+ "community"
30
+ ],
8
31
  "contributor": "Petri Lahdelma",
9
32
  "contributorUrl": "https://github.com/PetriLahdelma"
10
33
  }
package/src/audit/args.js CHANGED
@@ -37,8 +37,14 @@ Options:
37
37
  --token-kind <kind> Token kind: spacing, radius, typography, size, motion, all (default: spacing)
38
38
  --token-candidate-min-count <n> Minimum repeated raw value count for token candidates (default: 2)
39
39
  --scale <values|auto> Comma-separated scale values (default: 0,4,8,12,16,24,32);
40
- auto infers the scale from token sources, then scanned CSS
40
+ auto infers the scale from token sources, then --space-*/--spacing-*
41
+ custom properties and Sass variables in scanned CSS/SCSS, then
42
+ installed design-token packages, and reports where it came from
41
43
  --base-font-size <number> px base for rem/em conversion (default: 16)
44
+
45
+ Scans .css files, and .scss files when postcss-scss is installed. Reports drift by
46
+ value, by property and by file; text output prints histograms, markdown is PR-ready,
47
+ json is the stable 2.0 contract.
42
48
  `;
43
49
 
44
50
  function parseArgs(argv) {
@@ -3,6 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const {
5
5
  addDefinition,
6
+ collectScssTokens,
6
7
  createTokenKindMatcher,
7
8
  getNormalizedValueKeys,
8
9
  } = require('../utils/token-sources');
@@ -112,6 +113,16 @@ function collectTokenDefinitions(source, file, definitions, matchesKind, baseFon
112
113
  value: match[2].trim(),
113
114
  });
114
115
  }
116
+
117
+ for (const sassToken of collectScssTokens(source, matchesKind)) {
118
+ addDefinition(definitions, {
119
+ baseFontSize,
120
+ file,
121
+ source: file,
122
+ token: sassToken.token,
123
+ value: sassToken.value,
124
+ });
125
+ }
115
126
  }
116
127
 
117
128
  function collectTokenUses(source, file, uses, matchesKind) {
@@ -245,6 +256,8 @@ function buildReport({
245
256
  motionFindings,
246
257
  scale,
247
258
  scanScope,
259
+ scssFiles = 0,
260
+ scssSkipped = 0,
248
261
  templateFiles,
249
262
  tailwindFindings,
250
263
  tokenCandidateMinCount,
@@ -255,6 +268,9 @@ function buildReport({
255
268
  const offScaleValues = countByValue(cssFindings
256
269
  .filter((finding) => finding.type === 'off-scale' && finding.value)
257
270
  .map((finding) => finding.value));
271
+ const offScaleProperties = countByValue(cssFindings
272
+ .filter((finding) => finding.type === 'off-scale' && finding.property)
273
+ .map((finding) => finding.property));
258
274
  const tokenOpportunities = countByValue(cssFindings
259
275
  .filter((finding) => finding.type === 'token-opportunity' && finding.value)
260
276
  .map((finding) => finding.value));
@@ -306,12 +322,15 @@ function buildReport({
306
322
  findings: motionFindings.length,
307
323
  values: Object.fromEntries(sortCountMap(motionValues).slice(0, 10)),
308
324
  },
325
+ offScaleProperties: Object.fromEntries(sortCountMap(offScaleProperties).slice(0, 10)),
309
326
  offScaleValues: Object.fromEntries(sortCountMap(offScaleValues).slice(0, 10)),
310
327
  scale: scale || null,
311
328
  scaleCleanliness,
312
329
  scanScope,
313
330
  scanned: {
314
331
  cssFiles: cssFiles.length,
332
+ scssFiles,
333
+ scssSkipped,
315
334
  templateFiles: templateFiles.length,
316
335
  totalFiles,
317
336
  },
@@ -373,6 +392,7 @@ function toAuditContractReport(report) {
373
392
  scale: {
374
393
  cleanliness: report.scaleCleanliness,
375
394
  files: report.scale ? report.scale.files : [],
395
+ offScaleProperties: report.offScaleProperties || {},
376
396
  offScaleValues: report.offScaleValues,
377
397
  source: report.scale ? report.scale.source : 'default',
378
398
  tokenOpportunities: report.tokenOpportunities,
@@ -34,6 +34,7 @@ function renderHtml(report) {
34
34
  metricHtml('CSS files', report.cssFilesScanned),
35
35
  metricHtml('Template files', report.templateFilesScanned),
36
36
  '</section>',
37
+ renderHtmlTable('CSS Off-Scale Properties', ['Property', 'Count'], Object.entries(report.offScaleProperties || {})),
37
38
  renderHtmlTable('Top Affected Files', ['File', 'Findings'], report.topAffectedFiles.map(({ file, count }) => [file, count])),
38
39
  renderHtmlTable('Token Contract Sources', ['File', 'Format', 'Tokens'], report.tokenContract.sources.map((source) => [
39
40
  source.file,
@@ -21,6 +21,8 @@ function renderMarkdown(report) {
21
21
  '| --- | ---: |',
22
22
  `| CSS files scanned | ${report.cssFilesScanned} |`,
23
23
  `| Template files scanned | ${report.templateFilesScanned} |`,
24
+ ...(report.scanned && report.scanned.scssFiles > 0 ? [`| SCSS files scanned | ${report.scanned.scssFiles - report.scanned.scssSkipped} |`] : []),
25
+ ...(report.scanned && report.scanned.scssSkipped > 0 ? [`| SCSS files skipped (install postcss-scss) | ${report.scanned.scssSkipped} |`] : []),
24
26
  `| Files with issues | ${report.filesWithIssues} |`,
25
27
  `| Total findings | ${report.totalWarnings} |`,
26
28
  `| Scale cleanliness | ${report.scaleCleanliness}% |`,
@@ -38,6 +40,7 @@ function renderMarkdown(report) {
38
40
  lines.push('');
39
41
 
40
42
  appendMarkdownCounts(lines, 'CSS Off-Scale Values', report.offScaleValues);
43
+ appendMarkdownCounts(lines, 'CSS Off-Scale Properties', report.offScaleProperties, 'Property');
41
44
  appendMarkdownCounts(lines, 'CSS Token Opportunities', report.tokenOpportunities);
42
45
  appendMarkdownCounts(lines, 'Tailwind Class-String Drift', report.tailwindArbitraryValues);
43
46
  appendMarkdownCounts(lines, 'Motion Rhythm Drift', report.motion.values);
@@ -202,8 +205,8 @@ function appendBaselineMarkdown(lines, report) {
202
205
  }
203
206
  }
204
207
 
205
- function appendMarkdownCounts(lines, title, counts) {
206
- const entries = sortCountMap(counts);
208
+ function appendMarkdownCounts(lines, title, counts, label = 'Value') {
209
+ const entries = sortCountMap(counts || {});
207
210
 
208
211
  if (entries.length === 0) {
209
212
  return;
@@ -211,7 +214,7 @@ function appendMarkdownCounts(lines, title, counts) {
211
214
 
212
215
  lines.push(`## ${title}`);
213
216
  lines.push('');
214
- lines.push('| Value | Count |');
217
+ lines.push(`| ${label} | Count |`);
215
218
  lines.push('| --- | ---: |');
216
219
  for (const [value, count] of entries) {
217
220
  lines.push(`| \`${escapeMarkdown(value)}\` | ${count} |`);
@@ -19,6 +19,8 @@ function renderText(report) {
19
19
  '',
20
20
  ` CSS files scanned ${String(report.cssFilesScanned).padStart(4)}`,
21
21
  ` Template files scanned ${String(report.templateFilesScanned).padStart(4)}`,
22
+ ...(report.scanned && report.scanned.scssFiles > 0 ? [` SCSS files scanned ${String(report.scanned.scssFiles - report.scanned.scssSkipped).padStart(4)}`] : []),
23
+ ...(report.scanned && report.scanned.scssSkipped > 0 ? [` SCSS files skipped ${String(report.scanned.scssSkipped).padStart(4)} (install postcss-scss to audit them)`] : []),
22
24
  ` Files with issues ${String(report.filesWithIssues).padStart(4)}`,
23
25
  ` Scale cleanliness ${scoreBar(report.scaleCleanliness)} ${report.scaleCleanliness}%`,
24
26
  ];
@@ -31,6 +33,7 @@ function renderText(report) {
31
33
  lines.push('');
32
34
 
33
35
  appendHistogram(lines, 'CSS OFF-SCALE VALUES', report.offScaleValues);
36
+ appendHistogram(lines, 'CSS OFF-SCALE PROPERTIES', report.offScaleProperties);
34
37
  appendHistogram(lines, 'CSS TOKEN OPPORTUNITIES', report.tokenOpportunities);
35
38
  appendHistogram(lines, 'TAILWIND CLASS-STRING DRIFT', report.tailwindArbitraryValues);
36
39
  appendHistogram(lines, 'MOTION RHYTHM DRIFT', report.motion.values);
@@ -143,9 +146,9 @@ function appendBaselineText(lines, report) {
143
146
  }
144
147
  }
145
148
 
146
- function appendHistogram(lines, title, counts) {
147
- const entries = sortCountMap(counts);
148
- const total = sumCounts(counts);
149
+ function appendHistogram(lines, title, counts = {}) {
150
+ const entries = sortCountMap(counts || {});
151
+ const total = sumCounts(counts || {});
149
152
 
150
153
  if (entries.length === 0) {
151
154
  return;
@@ -18,7 +18,7 @@ const {
18
18
  } = require('./config');
19
19
  const { buildReport, collectTokenDefinitions } = require('./contract');
20
20
  const { DEFAULT_SCALE, formatPath } = require('./shared');
21
- const { scaleFromDefinitions } = require('../utils/scale-inference');
21
+ const { discoverTokenPackages, scaleFromDefinitions } = require('../utils/scale-inference');
22
22
  const {
23
23
  assertDirectory,
24
24
  collectCssFindings,
@@ -79,6 +79,8 @@ async function createAuditReport(options) {
79
79
  motionFindings,
80
80
  scale,
81
81
  scanScope,
82
+ scssFiles: cssResults.scssFiles || 0,
83
+ scssSkipped: cssResults.scssSkipped || 0,
82
84
  tailwindFindings: collectTailwindFindings(templateFiles, lintOptions),
83
85
  templateFiles,
84
86
  tokenCandidateMinCount: parsed.tokenCandidateMinCount,
@@ -98,6 +100,8 @@ async function createAuditReport(options) {
98
100
  return report;
99
101
  }
100
102
 
103
+ const NON_AUTHORED_SEGMENT = /(^|\/)(test|tests|__tests__|spec|specs|fixtures?|__fixtures__|__snapshots__|vendor|third[-_]?party|storybook-static)(\/|$)/i;
104
+
101
105
  /**
102
106
  * One project-level scale for the whole audit, with provenance.
103
107
  * "auto": external token sources, then spacing custom properties across the scanned
@@ -128,6 +132,11 @@ function resolveAuditScale({ baseFontSize, cssFiles, requested, tokenSourceResul
128
132
  const definitions = new Map();
129
133
  const matchesKind = createTokenKindMatcher('spacing');
130
134
  for (const filePath of cssFiles) {
135
+ // Test, fixture and vendored stylesheets often redefine tokens to exercise
136
+ // overrides; they are findings noise and must not shape the inferred scale.
137
+ if (NON_AUTHORED_SEGMENT.test(formatPath(filePath))) {
138
+ continue;
139
+ }
131
140
  let text;
132
141
  try {
133
142
  text = fs.readFileSync(filePath, 'utf8');
@@ -155,6 +164,20 @@ function resolveAuditScale({ baseFontSize, cssFiles, requested, tokenSourceResul
155
164
  }
156
165
  }
157
166
 
167
+ const packageSources = discoverTokenPackages(process.cwd());
168
+ if (packageSources.length > 0) {
169
+ const parsedPackages = parseTokenSources({ baseFontSize, sources: packageSources, tokenKind: 'spacing' });
170
+ const values = scaleFromDefinitions(parsedPackages.definitions, baseFontSize);
171
+ if (values) {
172
+ return {
173
+ files: parsedPackages.sources.map((source) => source.file),
174
+ source: 'token-package',
175
+ tokenCount: parsedPackages.definitions.size,
176
+ values,
177
+ };
178
+ }
179
+ }
180
+
158
181
  return { files: [], source: 'fallback', tokenCount: 0, values: DEFAULT_SCALE };
159
182
  }
160
183