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 +39 -0
- package/CONTRIBUTING.md +66 -51
- package/README.md +6 -6
- package/SECURITY.md +2 -2
- package/package.json +21 -27
- package/scales/community/product-decimal-10.json +27 -4
- package/src/audit/args.js +7 -1
- package/src/audit/contract.js +20 -0
- package/src/audit/render-html.js +1 -0
- package/src/audit/render-markdown.js +6 -3
- package/src/audit/render-text.js +6 -3
- package/src/audit/report.js +24 -1
- package/src/audit/scan.js +140 -10
- package/src/cli/init.js +2 -3
- package/src/cli/quickstart.js +11 -3
- package/src/index.js +0 -4
- package/src/utils/scale-inference.js +108 -12
- package/src/utils/token-packages.json +48 -0
- package/src/utils/token-sources.js +282 -3
- package/types/__checks__/consumer.ts +0 -6
- package/types/audit.d.ts +13 -2
- package/types/index.d.ts +0 -4
- package/types/shared.d.ts +2 -0
- package/src/configs/expanded.js +0 -27
- package/src/configs/expanded.mjs +0 -4
- package/src/configs/logical.js +0 -16
- package/src/configs/logical.mjs +0 -4
- package/src/configs/migration.js +0 -31
- package/src/configs/migration.mjs +0 -4
- package/src/configs/react-tailwind.js +0 -25
- package/src/configs/react-tailwind.mjs +0 -4
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
|
-
|
|
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
|
-
##
|
|
5
|
+
## Ways to contribute
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
npm run
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
npm
|
|
19
|
-
npm
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
## The local gate
|
|
25
33
|
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
42
|
+
Optional, depending on what you touched:
|
|
33
43
|
|
|
34
44
|
```bash
|
|
35
|
-
npm run
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
### Pull requests from forks
|
|
41
54
|
|
|
42
|
-
-
|
|
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
|
-
##
|
|
57
|
+
## How we write changes
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
67
|
+
**Prose without em dashes.** House style, applied to docs and messages alike.
|
|
54
68
|
|
|
55
|
-
## Semver
|
|
69
|
+
## Semver
|
|
56
70
|
|
|
57
|
-
- Patch
|
|
58
|
-
- Minor
|
|
59
|
-
- Major
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
- changing autofix behavior in a non-compatible way
|
|
65
|
-
- changing/removing exported config entry points
|
|
77
|
+
## Compatibility
|
|
66
78
|
|
|
67
|
-
|
|
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
|
-
|
|
84
|
+
## Review
|
|
70
85
|
|
|
71
|
-
|
|
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
|
-
##
|
|
88
|
+
## Releases
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
[](https://github.com/petrilahdelma/stylelint-plugin-rhythmguard/actions/workflows/ci.yml)
|
|
10
|
-
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
11
11
|
[](https://www.npmjs.com/package/stylelint-plugin-rhythmguard)
|
|
12
12
|
[](./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)
|
|
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`, `
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-rhythmguard",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
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": ">=
|
|
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
|
|
3
|
+
"description": "Decimal-friendly product spacing scale for dashboard-heavy UIs.",
|
|
4
4
|
"base": 10,
|
|
5
|
-
"steps": [
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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) {
|
package/src/audit/contract.js
CHANGED
|
@@ -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,
|
package/src/audit/render-html.js
CHANGED
|
@@ -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(
|
|
217
|
+
lines.push(`| ${label} | Count |`);
|
|
215
218
|
lines.push('| --- | ---: |');
|
|
216
219
|
for (const [value, count] of entries) {
|
|
217
220
|
lines.push(`| \`${escapeMarkdown(value)}\` | ${count} |`);
|
package/src/audit/render-text.js
CHANGED
|
@@ -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;
|
package/src/audit/report.js
CHANGED
|
@@ -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
|
|