stylelint-plugin-rhythmguard 1.3.0 → 1.4.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +103 -21
  3. package/assets/campaign-slides/scene-1.png +0 -0
  4. package/assets/campaign-slides/scene-2.png +0 -0
  5. package/assets/campaign-slides/scene-3.png +0 -0
  6. package/assets/campaign-slides/scene-4.png +0 -0
  7. package/assets/campaign-slides/scene-5.png +0 -0
  8. package/assets/campaign-slides/scene-6.png +0 -0
  9. package/assets/rhythmguard-campaign-60s-template.html +322 -0
  10. package/assets/rhythmguard-campaign-60s.gif +0 -0
  11. package/assets/rhythmguard-campaign-60s.webm +0 -0
  12. package/package.json +59 -14
  13. package/src/configs/expanded.js +27 -0
  14. package/src/configs/expanded.mjs +4 -0
  15. package/src/configs/logical.js +16 -0
  16. package/src/configs/logical.mjs +4 -0
  17. package/src/configs/migration.js +31 -0
  18. package/src/configs/migration.mjs +4 -0
  19. package/src/configs/recommended.mjs +4 -0
  20. package/src/configs/strict.mjs +4 -0
  21. package/src/configs/tailwind.mjs +4 -0
  22. package/src/eslint/index.js +16 -0
  23. package/src/eslint/index.mjs +8 -0
  24. package/src/eslint/rules/tailwind-class-use-scale.js +206 -0
  25. package/src/index.js +4 -0
  26. package/src/index.mjs +10 -0
  27. package/src/presets/index.mjs +12 -0
  28. package/src/rules/no-offscale-transform/index.js +68 -17
  29. package/src/rules/no-offscale-transform/index.mjs +7 -0
  30. package/src/rules/prefer-token/index.js +97 -22
  31. package/src/rules/prefer-token/index.mjs +7 -0
  32. package/src/rules/use-scale/index.js +67 -18
  33. package/src/rules/use-scale/index.mjs +7 -0
  34. package/src/utils/constants.js +81 -13
  35. package/src/utils/length.js +49 -11
  36. package/src/utils/options.js +335 -18
  37. package/src/utils/token-map.js +358 -0
  38. package/src/utils/value-utils.js +89 -10
package/CHANGELOG.md CHANGED
@@ -6,6 +6,36 @@ The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.0] - 2026-02-21
10
+
11
+ ### Added
12
+
13
+ - New shared configs:
14
+ - `stylelint-plugin-rhythmguard/configs/expanded`
15
+ - `stylelint-plugin-rhythmguard/configs/logical`
16
+ - `stylelint-plugin-rhythmguard/configs/migration`
17
+ - New ESLint companion export: `stylelint-plugin-rhythmguard/eslint` with rule:
18
+ - `rhythmguard-tailwind/tailwind-class-use-scale`
19
+ - Token migration source automation for `rhythmguard/prefer-token`:
20
+ - `tokenMapFromCssCustomProperties`
21
+ - `tokenMapFile`
22
+ - `tokenMapFromTailwindSpacing` + `tailwindConfigPath`
23
+ - ESM wrapper entry points for package root, configs, presets, rules, and ESLint companion.
24
+
25
+ ### Changed
26
+
27
+ - Broadened scale enforcement model with built-in property groups:
28
+ - `spacing`, `radius`, `typography`, `size`
29
+ - New per-property override options:
30
+ - `propertyGroups`
31
+ - `propertyScales`
32
+ - New math-function targeting options:
33
+ - `mathFunctionArguments`
34
+ - `ignoreMathFunctionArguments`
35
+ - New `unitStrategy` option (`convert` or `exact`) for non-convertible unit workflows.
36
+ - Compatibility updated to support Stylelint `^16 || ^17`.
37
+ - Tailwind token-map extraction now supports `.js`, `.cjs`, and `.mjs` config files and merges `theme.spacing` with `theme.extend.spacing`.
38
+
9
39
  ## [1.3.0] - 2026-02-17
10
40
 
11
41
  ### Added
package/README.md CHANGED
@@ -12,23 +12,22 @@ 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 spacing discipline across margin, padding, gap, inset, scroll spacing, and translate motion offsets.
15
+ `stylelint-plugin-rhythmguard` enforces scale and token discipline across spacing, radius, typography, size, and translate motion offsets.
16
16
 
17
- ## 60-second Demo
17
+ ## Demo
18
18
 
19
19
  <p align="center">
20
- <a href="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-campaign-60s.webm">
21
- <img src="https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-campaign-60s.gif" width="100%" alt="Rhythmguard 60-second campaign demo" />
20
+ <a href="./assets/rhythmguard-campaign-60s.webm">
21
+ <img src="./assets/rhythmguard-campaign-60s.gif" width="100%" alt="Rhythmguard 60-second demo" />
22
22
  </a>
23
23
  </p>
24
24
 
25
- - Full video (WebM): [rhythmguard-campaign-60s.webm](https://raw.githubusercontent.com/petrilahdelma/stylelint-plugin-rhythmguard/main/assets/rhythmguard-campaign-60s.webm)
26
-
27
25
  I built Rhythmguard after 20 years of watching teams ignore spacing scales and ship arbitrary pixel values everywhere.
28
26
 
29
27
  It is built for teams that want:
30
28
 
31
29
  - zero random spacing values in production CSS
30
+ - consistent numeric scales for radius, typography, and sizing primitives
32
31
  - token-first spacing workflows
33
32
  - predictable autofix behavior for large migrations
34
33
  - consistent layout rhythm across web surfaces
@@ -79,11 +78,44 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
79
78
  }
80
79
  ```
81
80
 
81
+ ### Expanded config
82
+
83
+ ```json
84
+ {
85
+ "extends": ["stylelint-plugin-rhythmguard/configs/expanded"]
86
+ }
87
+ ```
88
+
89
+ `expanded` enables scale enforcement for spacing + radius + typography + size property groups.
90
+
91
+ ### Logical config
92
+
93
+ ```json
94
+ {
95
+ "extends": ["stylelint-plugin-rhythmguard/configs/logical"]
96
+ }
97
+ ```
98
+
99
+ `logical` composes Rhythmguard strict mode with `stylelint-plugin-logical-css` recommended rules.
100
+
101
+ ### Migration config
102
+
103
+ ```json
104
+ {
105
+ "extends": ["stylelint-plugin-rhythmguard/configs/migration"]
106
+ }
107
+ ```
108
+
109
+ `migration` keeps on-scale numeric values temporarily while auto-building token mappings from CSS custom properties and optional Tailwind spacing config.
110
+
82
111
  Stable shared config entry points:
83
112
 
84
113
  - `stylelint-plugin-rhythmguard/configs/recommended`
85
114
  - `stylelint-plugin-rhythmguard/configs/strict`
86
115
  - `stylelint-plugin-rhythmguard/configs/tailwind`
116
+ - `stylelint-plugin-rhythmguard/configs/expanded`
117
+ - `stylelint-plugin-rhythmguard/configs/logical`
118
+ - `stylelint-plugin-rhythmguard/configs/migration`
87
119
 
88
120
  ### Full custom setup
89
121
 
@@ -95,13 +127,22 @@ Stable shared config entry points:
95
127
  true,
96
128
  {
97
129
  "preset": "rhythmic-4",
130
+ "propertyGroups": ["spacing", "radius"],
131
+ "propertyScales": {
132
+ "font-size": [12, 14, 16, 20, 24]
133
+ },
98
134
  "units": ["px", "rem", "em"],
135
+ "unitStrategy": "convert",
99
136
  "baseFontSize": 16,
100
137
  "tokenPattern": "^--space-",
101
138
  "tokenFunctions": ["var", "theme", "token"],
102
139
  "allowNegative": true,
103
140
  "allowPercentages": true,
104
- "fixToScale": true
141
+ "fixToScale": true,
142
+ "enforceInsideMathFunctions": true,
143
+ "mathFunctionArguments": {
144
+ "clamp": [1, 3]
145
+ }
105
146
  }
106
147
  ],
107
148
  "rhythmguard/prefer-token": [
@@ -109,6 +150,9 @@ Stable shared config entry points:
109
150
  {
110
151
  "tokenPattern": "^--space-",
111
152
  "allowNumericScale": false,
153
+ "tokenMapFromCssCustomProperties": true,
154
+ "tokenMapFromTailwindSpacing": true,
155
+ "tailwindConfigPath": "./tailwind.config.mjs",
112
156
  "tokenMap": {
113
157
  "4px": "var(--space-1)",
114
158
  "8px": "var(--space-2)",
@@ -162,7 +206,9 @@ Rhythmguard validates `secondaryOptions` for each rule before linting declaratio
162
206
 
163
207
  - Unknown option names fail fast with Stylelint invalid option warnings.
164
208
  - Invalid option shapes fail fast (for example string vs array mismatches).
165
- - `properties` string entries are validated against supported CSS spacing property names, plus `translate-x`, `translate-y`, and `translate-z`.
209
+ - `properties` string entries are validated against supported scale-targetable CSS property names.
210
+ - `propertyGroups` values are validated against built-in groups: `spacing`, `radius`, `typography`, and `size`.
211
+ - Math function argument maps are validated per function (`calc`, `clamp`, `min`, `max`) and positive 1-based argument indexes.
166
212
 
167
213
  Example typo that now fails immediately:
168
214
 
@@ -263,6 +309,10 @@ Checks:
263
309
  - `inset*`, `scroll-margin*`, `scroll-padding*`
264
310
  - `translate`, `translate-x`, `translate-y`, `translate-z`
265
311
  - `transform` translation functions (`translate`, `translateX`, `translateY`, `translateZ`, `translate3d`)
312
+ - optional property groups:
313
+ - `radius` (`border-radius*`, corner radii, `outline-offset`)
314
+ - `typography` (`font-size`, `line-height`, `letter-spacing`, `word-spacing`)
315
+ - `size` (`width`, `height`, min/max size, logical `inline-size`/`block-size`)
266
316
 
267
317
  Example:
268
318
 
@@ -288,6 +338,7 @@ Options:
288
338
  | `customScale` | `Array<number|string>` | `undefined` | Highest-priority custom scale override |
289
339
  | `scale` | `Array<number|string>` | `[0,4,8,12,16,24,32,40,48,64]` | Allowed spacing values |
290
340
  | `units` | `string[]` | `['px','rem','em']` | Units considered for scale enforcement |
341
+ | `unitStrategy` | `'convert' \| 'exact'` | `'convert'` | `convert`: compare via px conversion (`px/rem/em`). `exact`: compare against same-unit scale values (for example `vw`, `cqi`) |
291
342
  | `baseFontSize` | `number` | `16` | Used for `rem`/`em` conversion |
292
343
  | `tokenPattern` | `string` | `^--space-` | Regex for accepted token variable names |
293
344
  | `tokenFunctions` | `string[]` | `['var','theme','token']` | Functions treated as tokenized values |
@@ -295,7 +346,11 @@ Options:
295
346
  | `allowPercentages` | `boolean` | `true` | Allows `%` values without scale checks |
296
347
  | `fixToScale` | `boolean` | `true` | Enables nearest-value autofix |
297
348
  | `enforceInsideMathFunctions` | `boolean` | `false` | Lints `calc()/clamp()/min()/max()` internals |
349
+ | `mathFunctionArguments` | `Record<mathFn, number[]>` | `{}` | Restricts linting to specific 1-based argument indexes per math function |
350
+ | `ignoreMathFunctionArguments` | `Record<mathFn, number[]>` | `{}` | Excludes specific 1-based argument indexes per math function |
351
+ | `propertyGroups` | `Array<'spacing' \| 'radius' \| 'typography' \| 'size'>` | `['spacing']` | Selects built-in property groups when `properties` is not provided |
298
352
  | `properties` | `Array<string|RegExp>` | built-in spacing patterns | Override targeted property set; string values must be supported spacing property names |
353
+ | `propertyScales` | `Record<propertyOrRegex, scaleOrPreset>` | `{}` | Per-property scale overrides (supports exact names or `/regex/flags` keys) |
299
354
 
300
355
  ### `rhythmguard/prefer-token`
301
356
 
@@ -328,10 +383,20 @@ Options:
328
383
  | `customScale` | `Array<number|string>` | `undefined` | Highest-priority custom scale override |
329
384
  | `scale` | `Array<number|string>` | `[0,4,8,12,16,24,32,40,48,64]` | Used when `allowNumericScale` is enabled |
330
385
  | `baseFontSize` | `number` | `16` | Used for scale checks with `rem`/`em` |
386
+ | `unitStrategy` | `'convert' \| 'exact'` | `'convert'` | Matching strategy when `allowNumericScale` is enabled |
387
+ | `units` | `string[]` | `['px','rem','em']` | Units considered for numeric scale checks |
331
388
  | `enforceInsideMathFunctions` | `boolean` | `false` | Lints `calc()/clamp()/min()/max()` internals |
389
+ | `mathFunctionArguments` | `Record<mathFn, number[]>` | `{}` | Restricts linting to specific 1-based argument indexes per math function |
390
+ | `ignoreMathFunctionArguments` | `Record<mathFn, number[]>` | `{}` | Excludes specific 1-based argument indexes per math function |
332
391
  | `tokenMap` | `Record<string,string>` | `{}` | Enables autofix from raw value to token |
392
+ | `tokenMapFile` | `string` | `null` | JSON file path to merge additional token mappings |
393
+ | `tokenMapFromCssCustomProperties` | `boolean` | `false` | Auto-builds mappings from matching custom property declarations in the same stylesheet |
394
+ | `tokenMapFromTailwindSpacing` | `boolean` | `false` | Auto-builds mappings from `theme.spacing` and `theme.extend.spacing` in Tailwind config |
395
+ | `tailwindConfigPath` | `string` | `null` | Path to Tailwind config used by `tokenMapFromTailwindSpacing` (`.js`, `.cjs`, `.mjs`) |
333
396
  | `ignoreValues` | `string[]` | CSS global keywords + `auto` | Skips keyword literals |
397
+ | `propertyGroups` | `Array<'spacing' \| 'radius' \| 'typography' \| 'size'>` | `['spacing']` | Selects built-in property groups when `properties` is not provided |
334
398
  | `properties` | `Array<string|RegExp>` | built-in spacing patterns | Override targeted property set; string values must be supported spacing property names |
399
+ | `propertyScales` | `Record<propertyOrRegex, scaleOrPreset>` | `{}` | Per-property scale overrides for numeric migration mode |
335
400
 
336
401
  ### `rhythmguard/no-offscale-transform`
337
402
 
@@ -353,7 +418,7 @@ Example:
353
418
 
354
419
  Options:
355
420
 
356
- `rhythmguard/no-offscale-transform` accepts the same scale options as `rhythmguard/use-scale`, but only for transform translation properties. Its secondary options are also validated for unknown keys and invalid value shapes.
421
+ `rhythmguard/no-offscale-transform` accepts the same scale options as `rhythmguard/use-scale` (including `unitStrategy`, math argument targeting, and deterministic autofix), but only for transform translation properties. Its secondary options are also validated for unknown keys and invalid value shapes.
357
422
 
358
423
  ## Tailwind CSS Integration
359
424
 
@@ -371,7 +436,29 @@ Rhythmguard works well in Tailwind projects, but it enforces what Stylelint can
371
436
  - `class="p-4 gap-2"`
372
437
  - `class="p-[13px] translate-y-[18px]"`
373
438
 
374
- Those are not Stylelint declaration nodes, so they are outside this plugin's scope.
439
+ Those are not Stylelint declaration nodes, so they are outside Stylelint rule scope.
440
+
441
+ ### Companion ESLint layer for class strings
442
+
443
+ Rhythmguard now ships an ESLint companion export for class-string governance:
444
+
445
+ ```js
446
+ // eslint.config.js (flat config)
447
+ import rhythmguard from 'stylelint-plugin-rhythmguard/eslint';
448
+
449
+ export default [
450
+ {
451
+ plugins: {
452
+ 'rhythmguard-tailwind': rhythmguard,
453
+ },
454
+ rules: {
455
+ 'rhythmguard-tailwind/tailwind-class-use-scale': ['error', { scale: [0, 4, 8, 12, 16, 24, 32] }],
456
+ },
457
+ },
458
+ ];
459
+ ```
460
+
461
+ This rule targets arbitrary spacing utilities such as `p-[13px]`, `gap-[18px]`, `translate-x-[10px]`, and autofixes to the nearest configured scale value.
375
462
 
376
463
  ### Recommended stack for full Tailwind enforcement
377
464
 
@@ -390,7 +477,8 @@ Suggested setup:
390
477
 
391
478
  Then pair with:
392
479
 
393
- - `eslint-plugin-tailwindcss` for class-string rules (including arbitrary-value governance).
480
+ - `stylelint-plugin-rhythmguard/eslint` for arbitrary spacing class-string scale enforcement.
481
+ - `eslint-plugin-tailwindcss` for broader class-string linting and conventions.
394
482
  - `prettier-plugin-tailwindcss` for deterministic class ordering.
395
483
 
396
484
  Detailed setup reference: [`docs/TAILWIND.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/TAILWIND.md).
@@ -399,14 +487,7 @@ Detailed setup reference: [`docs/TAILWIND.md`](https://github.com/PetriLahdelma/
399
487
 
400
488
  By default, `tokenFunctions` includes `theme`, so values like `theme(spacing.4)` are treated as tokenized values.
401
489
 
402
- ### Product direction
403
-
404
- We should extend Tailwind coverage thoroughly, but in the right architecture:
405
-
406
- - keep `stylelint-plugin-rhythmguard` focused on CSS declaration enforcement
407
- - add a complementary Tailwind class-string layer (ESLint/plugin side) for utility classes
408
-
409
- This avoids brittle parsing hacks and gives full coverage without compromising rule quality.
490
+ This keeps CSS declaration enforcement and template class-string enforcement separated but coordinated.
410
491
 
411
492
  ## Programmatic Presets
412
493
 
@@ -417,6 +498,7 @@ console.log(rhythmguard.presets.listScalePresetNames());
417
498
  console.log(rhythmguard.presets.listCommunityScalePresetNames());
418
499
  console.log(rhythmguard.presets.getCommunityScaleMetadata('product-decimal-10'));
419
500
  console.log(rhythmguard.presets.scales['rhythmic-4']);
501
+ console.log(Object.keys(rhythmguard.eslint.rules));
420
502
  ```
421
503
 
422
504
  ## Autofix Philosophy
@@ -430,9 +512,9 @@ It will not guess token mappings without your map.
430
512
 
431
513
  ## Compatibility
432
514
 
433
- - Stylelint: `^16.0.0`
515
+ - Stylelint: `^16.0.0 || ^17.0.0`
434
516
  - Node.js: `>=18.18.0`
435
- - Module format: CommonJS plugin package
517
+ - Module format: dual `require` + `import` entry points (CommonJS + ESM wrappers)
436
518
  - Note: Stylelint `16.0.0` has known autofix/API behavior differences; CI enforces floor compatibility and runs non-blocking full-suite observability on the floor version.
437
519
 
438
520
  ## Development
@@ -0,0 +1,322 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Rhythmguard 60s Campaign Frame</title>
7
+ <style>
8
+ @font-face {
9
+ font-family: "GeistPixel";
10
+ src: url("../node_modules/geist/dist/fonts/geist-pixel/GeistPixel-Line.woff2") format("woff2");
11
+ font-weight: 400;
12
+ font-style: normal;
13
+ }
14
+
15
+ :root {
16
+ --bg: #000;
17
+ --fg: #fff;
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ html,
25
+ body {
26
+ width: 1920px;
27
+ height: 1080px;
28
+ margin: 0;
29
+ padding: 0;
30
+ overflow: hidden;
31
+ background: var(--bg);
32
+ color: var(--fg);
33
+ font-family: "GeistPixel", monospace;
34
+ }
35
+
36
+ body {
37
+ padding: 28px;
38
+ }
39
+
40
+ .frame {
41
+ width: 100%;
42
+ height: 100%;
43
+ border: 4px solid var(--fg);
44
+ display: grid;
45
+ grid-template-rows: 118px 1fr 110px;
46
+ }
47
+
48
+ .header {
49
+ border-bottom: 2px solid var(--fg);
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ padding: 0 30px;
54
+ font-size: 34px;
55
+ letter-spacing: 0.5px;
56
+ }
57
+
58
+ .main {
59
+ display: grid;
60
+ grid-template-columns: minmax(0, 2.1fr) minmax(0, 1fr);
61
+ gap: 28px;
62
+ padding: 28px 30px;
63
+ }
64
+
65
+ .hero {
66
+ border: 2px solid var(--fg);
67
+ padding: 26px;
68
+ display: grid;
69
+ grid-template-rows: auto auto auto 1fr;
70
+ gap: 18px;
71
+ }
72
+
73
+ .kicker {
74
+ font-size: 30px;
75
+ letter-spacing: 0.8px;
76
+ margin: 0;
77
+ }
78
+
79
+ .title {
80
+ font-size: 96px;
81
+ letter-spacing: 1px;
82
+ line-height: 1;
83
+ margin: 0;
84
+ }
85
+
86
+ .subtitle {
87
+ font-size: 42px;
88
+ letter-spacing: 0.8px;
89
+ margin: 0;
90
+ }
91
+
92
+ .terminal {
93
+ border: 2px solid var(--fg);
94
+ display: grid;
95
+ grid-template-rows: 50px 1fr;
96
+ min-height: 360px;
97
+ }
98
+
99
+ .terminal-bar {
100
+ border-bottom: 2px solid var(--fg);
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: space-between;
104
+ padding: 0 16px;
105
+ font-size: 24px;
106
+ }
107
+
108
+ .terminal-dots {
109
+ display: flex;
110
+ gap: 10px;
111
+ }
112
+
113
+ .terminal-dot {
114
+ width: 12px;
115
+ height: 12px;
116
+ border: 2px solid var(--fg);
117
+ }
118
+
119
+ pre {
120
+ margin: 0;
121
+ padding: 16px;
122
+ font-family: "GeistPixel", monospace;
123
+ font-size: 30px;
124
+ line-height: 1.45;
125
+ white-space: pre-wrap;
126
+ }
127
+
128
+ .side {
129
+ display: grid;
130
+ grid-template-rows: 1fr 1fr;
131
+ gap: 18px;
132
+ }
133
+
134
+ .card {
135
+ border: 2px solid var(--fg);
136
+ padding: 18px;
137
+ display: grid;
138
+ grid-template-rows: auto 1fr;
139
+ gap: 12px;
140
+ }
141
+
142
+ .card h2 {
143
+ margin: 0;
144
+ font-size: 36px;
145
+ letter-spacing: 0.8px;
146
+ }
147
+
148
+ .rules {
149
+ margin: 0;
150
+ padding-left: 24px;
151
+ font-size: 31px;
152
+ line-height: 1.5;
153
+ }
154
+
155
+ .meta {
156
+ margin: 0;
157
+ font-size: 30px;
158
+ line-height: 1.5;
159
+ }
160
+
161
+ .footer {
162
+ border-top: 2px solid var(--fg);
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: space-between;
166
+ padding: 0 30px;
167
+ font-size: 30px;
168
+ }
169
+ </style>
170
+ </head>
171
+ <body>
172
+ <div class="frame">
173
+ <header class="header">
174
+ <div id="step">STEP 1 / 6</div>
175
+ <div>Rhythmguard</div>
176
+ </header>
177
+ <main class="main">
178
+ <section class="hero">
179
+ <p class="kicker" id="kicker">INSTALL + STRICT CONFIG</p>
180
+ <h1 class="title" id="title">NO RANDOM 13PX.</h1>
181
+ <p class="subtitle" id="subtitle">SCALE OR TOKEN. PERIOD.</p>
182
+ <div class="terminal">
183
+ <div class="terminal-bar">
184
+ <div class="terminal-dots">
185
+ <span class="terminal-dot"></span>
186
+ <span class="terminal-dot"></span>
187
+ <span class="terminal-dot"></span>
188
+ </div>
189
+ <span id="terminal-title">terminal // setup</span>
190
+ </div>
191
+ <pre id="code"></pre>
192
+ </div>
193
+ </section>
194
+ <aside class="side">
195
+ <section class="card">
196
+ <h2>RULES</h2>
197
+ <ul class="rules">
198
+ <li>use-scale</li>
199
+ <li>prefer-token</li>
200
+ <li>no-offscale-transform</li>
201
+ </ul>
202
+ </section>
203
+ <section class="card">
204
+ <h2>WHY</h2>
205
+ <p class="meta" id="why"></p>
206
+ </section>
207
+ </aside>
208
+ </main>
209
+ <footer class="footer">
210
+ <div>stylelint-plugin-rhythmguard</div>
211
+ </footer>
212
+ </div>
213
+
214
+ <script>
215
+ const scenes = [
216
+ {
217
+ kicker: "INSTALL + STRICT CONFIG",
218
+ title: "NO RANDOM 13PX.",
219
+ subtitle: "SCALE OR TOKEN. PERIOD.",
220
+ terminalTitle: "terminal // setup",
221
+ code: [
222
+ "$ npm i -D stylelint stylelint-plugin-rhythmguard",
223
+ "$ cat .stylelintrc.json",
224
+ "{",
225
+ ' "extends": ["stylelint-plugin-rhythmguard/configs/strict"]',
226
+ "}",
227
+ '$ npx stylelint "src/**/*.css"',
228
+ ],
229
+ why: "Shared config replaces ad-hoc spacing rules across files.",
230
+ },
231
+ {
232
+ kicker: "RULE 1 // USE-SCALE",
233
+ title: "LOCK TO SCALE.",
234
+ subtitle: "EVERY SPACING VALUE GETS CHECKED.",
235
+ terminalTitle: "report // use-scale",
236
+ code: [
237
+ "Button.css:12",
238
+ " Expected spacing from configured scale.",
239
+ " ✕ margin-top: 13px",
240
+ " ✓ margin-top: 12px",
241
+ "",
242
+ "Autofix maps to nearest safe scale step.",
243
+ ],
244
+ why: "Scale discipline keeps layouts predictable in large systems.",
245
+ },
246
+ {
247
+ kicker: "RULE 2 // PREFER-TOKEN",
248
+ title: "TOKENS FIRST.",
249
+ subtitle: "RAW PIXELS BECOME DESIGN TOKENS.",
250
+ terminalTitle: "report // prefer-token",
251
+ code: [
252
+ "Card.css:8",
253
+ " Expected token, not raw literal.",
254
+ " ✕ padding: 16px",
255
+ " ✓ padding: var(--space-4)",
256
+ "",
257
+ "tokenMap applies safe automatic replacements.",
258
+ ],
259
+ why: "Token usage makes theme changes and audits low-risk.",
260
+ },
261
+ {
262
+ kicker: "RULE 3 // TRANSFORM OFFSETS",
263
+ title: "MOTION IN RHYTHM.",
264
+ subtitle: "TRANSLATES STAY ON THE SAME SCALE.",
265
+ terminalTitle: "report // no-offscale-transform",
266
+ code: [
267
+ "Toast.css:22",
268
+ " Off-scale translate value detected.",
269
+ " ✕ transform: translateY(7px)",
270
+ " ✓ transform: translateY(8px)",
271
+ "",
272
+ "Motion offsets align with layout spacing values.",
273
+ ],
274
+ why: "Spacing + motion consistency improves visual quality.",
275
+ },
276
+ {
277
+ kicker: "AUTOFIX WORKFLOW",
278
+ title: "FIX FAST, SAFELY.",
279
+ subtitle: "MIGRATE LEGACY CSS WITHOUT DRAMA.",
280
+ terminalTitle: "autofix // migration",
281
+ code: [
282
+ "$ npx stylelint \"src/**/*.css\" --fix",
283
+ "Applied fixes:",
284
+ " - use-scale: 124",
285
+ " - prefer-token: 98",
286
+ " - no-offscale-transform: 19",
287
+ "",
288
+ "Review diff and ship.",
289
+ ],
290
+ why: "Autofix handles repetitive cleanup at production scale.",
291
+ },
292
+ {
293
+ kicker: "READY TO SHIP",
294
+ title: "RHYTHMGUARD",
295
+ subtitle: "SPACING GOVERNANCE FOR REAL-WORLD TEAMS.",
296
+ terminalTitle: "next // adopt",
297
+ code: [
298
+ "Extends available:",
299
+ " - configs/recommended",
300
+ " - configs/strict",
301
+ " - configs/tailwind",
302
+ "",
303
+ "No random spacing values in production CSS.",
304
+ ],
305
+ why: "Consistent spacing rules reduce regressions release after release.",
306
+ },
307
+ ];
308
+
309
+ const sceneQuery = Number(new URLSearchParams(window.location.search).get("scene") || "1");
310
+ const index = Math.min(Math.max(sceneQuery, 1), scenes.length) - 1;
311
+ const scene = scenes[index];
312
+
313
+ document.getElementById("step").textContent = `STEP ${index + 1} / ${scenes.length}`;
314
+ document.getElementById("kicker").textContent = scene.kicker;
315
+ document.getElementById("title").textContent = scene.title;
316
+ document.getElementById("subtitle").textContent = scene.subtitle;
317
+ document.getElementById("terminal-title").textContent = scene.terminalTitle;
318
+ document.getElementById("code").textContent = scene.code.join("\n");
319
+ document.getElementById("why").textContent = scene.why;
320
+ </script>
321
+ </body>
322
+ </html>
Binary file