stylelint-plugin-rhythmguard 1.2.1 → 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 +48 -0
  2. package/README.md +130 -19
  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 +60 -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 +81 -18
  29. package/src/rules/no-offscale-transform/index.mjs +7 -0
  30. package/src/rules/prefer-token/index.js +110 -23
  31. package/src/rules/prefer-token/index.mjs +7 -0
  32. package/src/rules/use-scale/index.js +80 -19
  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 +626 -9
  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,53 @@ 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
+
39
+ ## [1.3.0] - 2026-02-17
40
+
41
+ ### Added
42
+
43
+ - Strict `secondaryOptions` validation for all three rules:
44
+ - `rhythmguard/use-scale`
45
+ - `rhythmguard/prefer-token`
46
+ - `rhythmguard/no-offscale-transform`
47
+ - Invalid option names (for example `sevverity`) now fail with Stylelint invalid option warnings instead of silently being ignored.
48
+ - Type/shape validation for option payloads (for example `properties` must be an array, `tokenMap` must be an object).
49
+ - Regression tests for invalid secondary option names and option value shapes.
50
+
51
+ ### Changed
52
+
53
+ - Added `known-css-properties` as a direct runtime dependency to guarantee `properties` option validation in consumer installs.
54
+ - `properties` option validation now checks supported spacing property names against known CSS property metadata (plus `translate-x`, `translate-y`, `translate-z`).
55
+
9
56
  ## [1.2.1] - 2026-02-17
10
57
 
11
58
  ### Fixed
@@ -15,6 +62,7 @@ The format follows Keep a Changelog principles and semantic versioning.
15
62
  - `rhythmguard/prefer-token` now supports `enforceInsideMathFunctions` for optional math-function enforcement.
16
63
  - Hardened `var()` token argument detection to parse the first argument structurally (rather than comma string splitting).
17
64
  - npm README link integrity: docs links now resolve to absolute GitHub URLs from the npm package page.
65
+ - Release workflow now detects missing `NPM_TOKEN` and skips publish cleanly with an explicit notice instead of failing.
18
66
 
19
67
  ### Added
20
68
 
package/README.md CHANGED
@@ -12,13 +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
+
17
+ ## Demo
18
+
19
+ <p align="center">
20
+ <a href="./assets/rhythmguard-campaign-60s.webm">
21
+ <img src="./assets/rhythmguard-campaign-60s.gif" width="100%" alt="Rhythmguard 60-second demo" />
22
+ </a>
23
+ </p>
16
24
 
17
25
  I built Rhythmguard after 20 years of watching teams ignore spacing scales and ship arbitrary pixel values everywhere.
18
26
 
19
27
  It is built for teams that want:
20
28
 
21
29
  - zero random spacing values in production CSS
30
+ - consistent numeric scales for radius, typography, and sizing primitives
22
31
  - token-first spacing workflows
23
32
  - predictable autofix behavior for large migrations
24
33
  - consistent layout rhythm across web surfaces
@@ -69,11 +78,44 @@ npm install --save-dev stylelint stylelint-plugin-rhythmguard
69
78
  }
70
79
  ```
71
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
+
72
111
  Stable shared config entry points:
73
112
 
74
113
  - `stylelint-plugin-rhythmguard/configs/recommended`
75
114
  - `stylelint-plugin-rhythmguard/configs/strict`
76
115
  - `stylelint-plugin-rhythmguard/configs/tailwind`
116
+ - `stylelint-plugin-rhythmguard/configs/expanded`
117
+ - `stylelint-plugin-rhythmguard/configs/logical`
118
+ - `stylelint-plugin-rhythmguard/configs/migration`
77
119
 
78
120
  ### Full custom setup
79
121
 
@@ -85,13 +127,22 @@ Stable shared config entry points:
85
127
  true,
86
128
  {
87
129
  "preset": "rhythmic-4",
130
+ "propertyGroups": ["spacing", "radius"],
131
+ "propertyScales": {
132
+ "font-size": [12, 14, 16, 20, 24]
133
+ },
88
134
  "units": ["px", "rem", "em"],
135
+ "unitStrategy": "convert",
89
136
  "baseFontSize": 16,
90
137
  "tokenPattern": "^--space-",
91
138
  "tokenFunctions": ["var", "theme", "token"],
92
139
  "allowNegative": true,
93
140
  "allowPercentages": true,
94
- "fixToScale": true
141
+ "fixToScale": true,
142
+ "enforceInsideMathFunctions": true,
143
+ "mathFunctionArguments": {
144
+ "clamp": [1, 3]
145
+ }
95
146
  }
96
147
  ],
97
148
  "rhythmguard/prefer-token": [
@@ -99,6 +150,9 @@ Stable shared config entry points:
99
150
  {
100
151
  "tokenPattern": "^--space-",
101
152
  "allowNumericScale": false,
153
+ "tokenMapFromCssCustomProperties": true,
154
+ "tokenMapFromTailwindSpacing": true,
155
+ "tailwindConfigPath": "./tailwind.config.mjs",
102
156
  "tokenMap": {
103
157
  "4px": "var(--space-1)",
104
158
  "8px": "var(--space-2)",
@@ -146,6 +200,26 @@ Scale resolution precedence:
146
200
  3. `preset`
147
201
  4. default `rhythmic-4` scale
148
202
 
203
+ ## Option Validation
204
+
205
+ Rhythmguard validates `secondaryOptions` for each rule before linting declarations.
206
+
207
+ - Unknown option names fail fast with Stylelint invalid option warnings.
208
+ - Invalid option shapes fail fast (for example string vs array mismatches).
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.
212
+
213
+ Example typo that now fails immediately:
214
+
215
+ ```json
216
+ {
217
+ "rules": {
218
+ "rhythmguard/use-scale": [true, { "sevverity": "warning" }]
219
+ }
220
+ }
221
+ ```
222
+
149
223
  ## Built-in Scale Presets
150
224
 
151
225
  | Preset | Pattern | Scale |
@@ -235,6 +309,10 @@ Checks:
235
309
  - `inset*`, `scroll-margin*`, `scroll-padding*`
236
310
  - `translate`, `translate-x`, `translate-y`, `translate-z`
237
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`)
238
316
 
239
317
  Example:
240
318
 
@@ -260,6 +338,7 @@ Options:
260
338
  | `customScale` | `Array<number|string>` | `undefined` | Highest-priority custom scale override |
261
339
  | `scale` | `Array<number|string>` | `[0,4,8,12,16,24,32,40,48,64]` | Allowed spacing values |
262
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`) |
263
342
  | `baseFontSize` | `number` | `16` | Used for `rem`/`em` conversion |
264
343
  | `tokenPattern` | `string` | `^--space-` | Regex for accepted token variable names |
265
344
  | `tokenFunctions` | `string[]` | `['var','theme','token']` | Functions treated as tokenized values |
@@ -267,7 +346,11 @@ Options:
267
346
  | `allowPercentages` | `boolean` | `true` | Allows `%` values without scale checks |
268
347
  | `fixToScale` | `boolean` | `true` | Enables nearest-value autofix |
269
348
  | `enforceInsideMathFunctions` | `boolean` | `false` | Lints `calc()/clamp()/min()/max()` internals |
270
- | `properties` | `Array<string|RegExp>` | built-in spacing patterns | Override targeted property set |
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 |
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) |
271
354
 
272
355
  ### `rhythmguard/prefer-token`
273
356
 
@@ -300,10 +383,20 @@ Options:
300
383
  | `customScale` | `Array<number|string>` | `undefined` | Highest-priority custom scale override |
301
384
  | `scale` | `Array<number|string>` | `[0,4,8,12,16,24,32,40,48,64]` | Used when `allowNumericScale` is enabled |
302
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 |
303
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 |
304
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`) |
305
396
  | `ignoreValues` | `string[]` | CSS global keywords + `auto` | Skips keyword literals |
306
- | `properties` | `Array<string|RegExp>` | built-in spacing patterns | Override targeted property set |
397
+ | `propertyGroups` | `Array<'spacing' \| 'radius' \| 'typography' \| 'size'>` | `['spacing']` | Selects built-in property groups when `properties` is not provided |
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 |
307
400
 
308
401
  ### `rhythmguard/no-offscale-transform`
309
402
 
@@ -325,7 +418,7 @@ Example:
325
418
 
326
419
  Options:
327
420
 
328
- `rhythmguard/no-offscale-transform` accepts the same scale options as `rhythmguard/use-scale`, but only for transform translation properties.
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.
329
422
 
330
423
  ## Tailwind CSS Integration
331
424
 
@@ -343,7 +436,29 @@ Rhythmguard works well in Tailwind projects, but it enforces what Stylelint can
343
436
  - `class="p-4 gap-2"`
344
437
  - `class="p-[13px] translate-y-[18px]"`
345
438
 
346
- 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.
347
462
 
348
463
  ### Recommended stack for full Tailwind enforcement
349
464
 
@@ -362,7 +477,8 @@ Suggested setup:
362
477
 
363
478
  Then pair with:
364
479
 
365
- - `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.
366
482
  - `prettier-plugin-tailwindcss` for deterministic class ordering.
367
483
 
368
484
  Detailed setup reference: [`docs/TAILWIND.md`](https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/TAILWIND.md).
@@ -371,14 +487,7 @@ Detailed setup reference: [`docs/TAILWIND.md`](https://github.com/PetriLahdelma/
371
487
 
372
488
  By default, `tokenFunctions` includes `theme`, so values like `theme(spacing.4)` are treated as tokenized values.
373
489
 
374
- ### Product direction
375
-
376
- We should extend Tailwind coverage thoroughly, but in the right architecture:
377
-
378
- - keep `stylelint-plugin-rhythmguard` focused on CSS declaration enforcement
379
- - add a complementary Tailwind class-string layer (ESLint/plugin side) for utility classes
380
-
381
- 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.
382
491
 
383
492
  ## Programmatic Presets
384
493
 
@@ -389,6 +498,7 @@ console.log(rhythmguard.presets.listScalePresetNames());
389
498
  console.log(rhythmguard.presets.listCommunityScalePresetNames());
390
499
  console.log(rhythmguard.presets.getCommunityScaleMetadata('product-decimal-10'));
391
500
  console.log(rhythmguard.presets.scales['rhythmic-4']);
501
+ console.log(Object.keys(rhythmguard.eslint.rules));
392
502
  ```
393
503
 
394
504
  ## Autofix Philosophy
@@ -402,9 +512,9 @@ It will not guess token mappings without your map.
402
512
 
403
513
  ## Compatibility
404
514
 
405
- - Stylelint: `^16.0.0`
515
+ - Stylelint: `^16.0.0 || ^17.0.0`
406
516
  - Node.js: `>=18.18.0`
407
- - Module format: CommonJS plugin package
517
+ - Module format: dual `require` + `import` entry points (CommonJS + ESM wrappers)
408
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.
409
519
 
410
520
  ## Development
@@ -441,8 +551,9 @@ Detailed methodology and custom args are documented in [`docs/BENCHMARKING.md`](
441
551
  1. Create a GitHub release.
442
552
  2. `release.yml` runs the Node/Stylelint matrix validation.
443
553
  3. A tarball smoke test validates package exports and install behavior.
444
- 4. The package is published to npm with provenance (`npm publish --provenance`).
445
- 5. `post-publish-smoke.yml` verifies the published npm version can be installed and run in a clean project.
554
+ 4. If `NPM_TOKEN` is configured in repository secrets, the package is published to npm with provenance (`npm publish --provenance`).
555
+ 5. If `NPM_TOKEN` is not configured, publish is skipped with an explicit workflow notice.
556
+ 6. `post-publish-smoke.yml` verifies the published npm version can be installed and run in a clean project (and skips cleanly if the version is not on npm).
446
557
 
447
558
  ## Support and Bug Reports
448
559
 
@@ -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>