stylelint-plugin-rhythmguard 2.1.0 → 3.0.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 (39) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/CONTRIBUTING.md +66 -50
  3. package/README.md +17 -6
  4. package/SECURITY.md +2 -2
  5. package/package.json +27 -27
  6. package/scales/community/product-decimal-10.json +27 -4
  7. package/src/audit/args.js +6 -1
  8. package/src/audit/config.js +15 -5
  9. package/src/audit/contract.js +20 -0
  10. package/src/audit/render-markdown.js +13 -0
  11. package/src/audit/render-text.js +9 -1
  12. package/src/audit/report.js +105 -8
  13. package/src/audit/scan.js +67 -10
  14. package/src/cli/index.js +11 -5
  15. package/src/cli/init.js +7 -4
  16. package/src/cli/quickstart.js +207 -0
  17. package/src/configs/embed.js +24 -0
  18. package/src/configs/{logical.mjs → embed.mjs} +1 -1
  19. package/src/index.js +1 -4
  20. package/src/rules/no-offscale-transform/index.js +22 -0
  21. package/src/rules/prefer-token/index.js +28 -0
  22. package/src/rules/use-scale/index.js +34 -4
  23. package/src/utils/length.js +16 -0
  24. package/src/utils/options.js +53 -3
  25. package/src/utils/scale-inference.js +351 -0
  26. package/src/utils/token-map.js +4 -2
  27. package/src/utils/token-packages.json +48 -0
  28. package/src/utils/token-sources.js +313 -4
  29. package/types/__checks__/consumer.ts +10 -6
  30. package/types/audit.d.ts +21 -1
  31. package/types/index.d.ts +4 -4
  32. package/types/shared.d.ts +24 -1
  33. package/src/configs/expanded.js +0 -27
  34. package/src/configs/expanded.mjs +0 -4
  35. package/src/configs/logical.js +0 -16
  36. package/src/configs/migration.js +0 -31
  37. package/src/configs/migration.mjs +0 -4
  38. package/src/configs/react-tailwind.js +0 -25
  39. package/src/configs/react-tailwind.mjs +0 -4
@@ -31,7 +31,14 @@ const TOKEN_KIND_PATTERNS = Object.freeze({
31
31
  radius: /^--radius-/,
32
32
  motion: /^--(?:motion|duration|delay|ease|easing)-/,
33
33
  size: /^--(?:size|width|height|container)-/,
34
- spacing: /^--(?:space|spacing)-/,
34
+ // Anchored or prefixed (--spacing-4, --lb-spacing-md, bare Tailwind v4 --spacing), never letter-/word-spacing.
35
+ // CSS custom properties (--spacing-4, --lb-spacing-md, bare --spacing) and Sass
36
+ // variables or map entries ($spacer, $spacers.3, $system-spacing.small.2).
37
+ // Custom properties may carry a namespace (--lb-spacing-md, --mantine-spacing-xs, bare
38
+ // --spacing) but never letter-/word-spacing. Sass names must start with the scale word
39
+ // (an optional `system-` prefix allowed): $spacer, $spacers.3, $spacing-01, $system-spacing.
40
+ // Component variables such as $dropdown-spacer or $card-spacer-y are not scale tokens.
41
+ spacing: /^(?:\$(?:system-)?(?:space|spacing|spacer)s?(?:-|$|\.)|--(?:[\w-]*-)?(?<!letter-)(?<!word-)(?:space|spacing)(?:-|$))/,
35
42
  typography: /^--(?:font|font-size|line-height|leading|tracking|typography)-/,
36
43
  });
37
44
 
@@ -61,6 +68,15 @@ function normalizeTokenKind(kind) {
61
68
  return normalized;
62
69
  }
63
70
 
71
+ function createPatternMatcher(pattern, fallback) {
72
+ try {
73
+ const regex = new RegExp(pattern);
74
+ return (token) => regex.test(token);
75
+ } catch {
76
+ return fallback;
77
+ }
78
+ }
79
+
64
80
  function createTokenKindMatcher(kind) {
65
81
  const normalizedKind = normalizeTokenKind(kind);
66
82
  const pattern = TOKEN_KIND_PATTERNS[normalizedKind] || TOKEN_KIND_PATTERNS.spacing;
@@ -108,16 +124,19 @@ function parseTokenSources({
108
124
  }
109
125
 
110
126
  let tokens = [];
127
+ const matchesSource = normalizedSource.tokenPattern
128
+ ? createPatternMatcher(normalizedSource.tokenPattern, matchesKind)
129
+ : matchesKind;
111
130
  try {
112
131
  if (normalizedSource.format === 'css') {
113
- tokens = collectCssTokens(text, matchesKind);
132
+ tokens = collectCssTokens(text, matchesSource);
114
133
  } else {
115
134
  const parsed = JSON.parse(text);
116
135
  const detectedFormat = normalizedSource.requestedFormat === 'auto'
117
136
  ? detectJsonTokenFormat(parsed)
118
137
  : normalizedSource.format;
119
138
  sourceReport.format = detectedFormat;
120
- tokens = collectJsonTokens(parsed, matchesKind);
139
+ tokens = collectJsonTokens(parsed, matchesSource);
121
140
  }
122
141
  } catch (err) {
123
142
  const warning = `Unable to parse token source ${normalizedSource.displayPath}: ${err.message}`;
@@ -167,6 +186,9 @@ function normalizeTokenSource(source) {
167
186
  format: requestedFormat === 'auto' ? detectSourceFormat(resolvedPath) : requestedFormat,
168
187
  requestedFormat,
169
188
  resolvedPath,
189
+ // Optional per-source override for packages that name spacing tokens differently
190
+ // (Primer's --base-size-*). Falls back to the kind matcher when absent.
191
+ tokenPattern: typeof source.tokenPattern === 'string' && source.tokenPattern ? source.tokenPattern : null,
170
192
  };
171
193
  }
172
194
 
@@ -230,9 +252,266 @@ function collectCssTokens(source, matchesKind) {
230
252
  });
231
253
  }
232
254
 
255
+ tokens.push(...collectScssTokens(source, matchesKind));
256
+ return tokens;
257
+ }
258
+
259
+ /**
260
+ * Sass variables and maps as token sources. Handles `$spacer: 1rem`, maps such
261
+ * as `$spacers: (1: $spacer * .25, ...)` including nested maps, variable
262
+ * references, `* / + -` arithmetic and `math.div()`. Anything it cannot
263
+ * evaluate (function calls, strings, keywords, interpolated keys, cycles) is
264
+ * skipped rather than guessed. Token names are `$name` or `$name.key.path`.
265
+ */
266
+ function collectScssTokens(source, matchesKind) {
267
+ const declarations = parseScssDeclarations(source);
268
+ const cache = new Map();
269
+
270
+ const resolveVariable = (name, stack) => {
271
+ if (cache.has(name)) {
272
+ return cache.get(name);
273
+ }
274
+ if (stack.has(name) || !declarations.has(name)) {
275
+ return null;
276
+ }
277
+ stack.add(name);
278
+ const raw = declarations.get(name);
279
+ const value = isScssMap(raw) ? null : evaluateScssExpression(raw, resolveVariable, stack);
280
+ stack.delete(name);
281
+ cache.set(name, value);
282
+ return value;
283
+ };
284
+
285
+ const tokens = [];
286
+ const push = (tokenName, value) => {
287
+ if (!value || !matchesKind(tokenName)) {
288
+ return;
289
+ }
290
+ tokens.push({ token: tokenName, value: formatScssValue(value) });
291
+ };
292
+
293
+ for (const [name, raw] of declarations) {
294
+ const tokenName = `$${name}`;
295
+ if (isScssMap(raw)) {
296
+ walkScssMap(raw, [tokenName], (pathName, expression) => {
297
+ push(pathName, evaluateScssExpression(expression, resolveVariable, new Set()));
298
+ });
299
+ continue;
300
+ }
301
+ push(tokenName, resolveVariable(name, new Set()));
302
+ }
303
+
304
+ return tokens;
305
+ }
306
+
307
+ function stripScssComments(source) {
308
+ return source
309
+ .replace(/\/\*[\s\S]*?\*\//g, '')
310
+ .replace(/(^|[^:])\/\/[^\n]*/g, '$1');
311
+ }
312
+
313
+ function parseScssDeclarations(source) {
314
+ const text = stripScssComments(source);
315
+ const declarations = new Map();
316
+ const startPattern = /(?:^|\n)[ \t]*\$([\w-]+)[ \t]*:/g;
317
+ let match;
318
+
319
+ while ((match = startPattern.exec(text)) !== null) {
320
+ const name = match[1];
321
+ let index = match.index + match[0].length;
322
+ let depth = 0;
323
+ let quote = null;
324
+ let value = '';
325
+
326
+ for (; index < text.length; index += 1) {
327
+ const char = text[index];
328
+ if (quote) {
329
+ value += char;
330
+ if (char === quote) quote = null;
331
+ continue;
332
+ }
333
+ if (char === '"' || char === "'") {
334
+ quote = char;
335
+ } else if (char === '(') {
336
+ depth += 1;
337
+ } else if (char === ')') {
338
+ depth -= 1;
339
+ } else if (char === ';' && depth === 0) {
340
+ break;
341
+ }
342
+ value += char;
343
+ }
344
+
345
+ startPattern.lastIndex = index;
346
+ const cleaned = value.replace(/!(default|global)\b/g, '').trim();
347
+ if (cleaned && !declarations.has(name)) {
348
+ declarations.set(name, cleaned);
349
+ }
350
+ }
351
+
352
+ return declarations;
353
+ }
354
+
355
+ function isScssMap(raw) {
356
+ return raw.startsWith('(') && raw.endsWith(')') && /:/.test(raw);
357
+ }
358
+
359
+ function splitTopLevel(text, separator) {
360
+ const parts = [];
361
+ let depth = 0;
362
+ let quote = null;
363
+ let current = '';
364
+ for (const char of text) {
365
+ if (quote) {
366
+ current += char;
367
+ if (char === quote) quote = null;
368
+ continue;
369
+ }
370
+ if (char === '"' || char === "'") quote = char;
371
+ else if (char === '(') depth += 1;
372
+ else if (char === ')') depth -= 1;
373
+ if (char === separator && depth === 0) {
374
+ parts.push(current);
375
+ current = '';
376
+ continue;
377
+ }
378
+ current += char;
379
+ }
380
+ if (current.trim()) parts.push(current);
381
+ return parts;
382
+ }
383
+
384
+ function walkScssMap(raw, pathSegments, visit) {
385
+ const inner = raw.slice(1, -1);
386
+ for (const entry of splitTopLevel(inner, ',')) {
387
+ const pair = splitTopLevel(entry, ':');
388
+ if (pair.length < 2) continue;
389
+ const key = pair[0].trim().replace(/^["']|["']$/g, '');
390
+ const expression = pair.slice(1).join(':').trim();
391
+ if (!key || key.includes('#{')) continue;
392
+ const nextPath = [...pathSegments, key];
393
+ if (isScssMap(expression)) {
394
+ walkScssMap(expression, nextPath, visit);
395
+ } else {
396
+ visit(nextPath.join('.'), expression);
397
+ }
398
+ }
399
+ }
400
+
401
+ const SCSS_TOKEN_PATTERN = /\s*(?:(-?(?:\d+\.?\d*|\.\d+)[a-zA-Z%]*)|(\$[\w-]+)|([a-zA-Z_][\w.-]*)\s*\(|([()*/+,-]))/y;
402
+
403
+ function tokenizeScssExpression(expression) {
404
+ const tokens = [];
405
+ let index = 0;
406
+ while (index < expression.length) {
407
+ SCSS_TOKEN_PATTERN.lastIndex = index;
408
+ const match = SCSS_TOKEN_PATTERN.exec(expression);
409
+ if (!match) {
410
+ if (/\s/.test(expression[index])) {
411
+ index += 1;
412
+ continue;
413
+ }
414
+ return null;
415
+ }
416
+ index = SCSS_TOKEN_PATTERN.lastIndex;
417
+ if (match[1] !== undefined) tokens.push({ type: 'number', raw: match[1] });
418
+ else if (match[2] !== undefined) tokens.push({ type: 'var', name: match[2].slice(1) });
419
+ else if (match[3] !== undefined) tokens.push({ type: 'call', name: match[3] });
420
+ else tokens.push({ type: 'op', value: match[4] });
421
+ }
233
422
  return tokens;
234
423
  }
235
424
 
425
+ function evaluateScssExpression(expression, resolveVariable, stack) {
426
+ const tokens = tokenizeScssExpression(expression.trim());
427
+ if (!tokens || tokens.length === 0) {
428
+ return null;
429
+ }
430
+ let position = 0;
431
+ const peek = () => tokens[position];
432
+ const next = () => tokens[position++];
433
+
434
+ const parseNumber = (raw) => {
435
+ const parsed = parseLengthToken(raw);
436
+ if (parsed) return { number: parsed.number, unit: parsed.unit };
437
+ const numeric = Number(raw);
438
+ return Number.isFinite(numeric) ? { number: numeric, unit: '' } : null;
439
+ };
440
+
441
+ const combine = (left, op, right) => {
442
+ if (!left || !right) return null;
443
+ if (op === '*') {
444
+ if (left.unit && right.unit) return null;
445
+ return { number: left.number * right.number, unit: left.unit || right.unit };
446
+ }
447
+ if (op === '/') {
448
+ if (right.number === 0) return null;
449
+ if (right.unit && right.unit !== left.unit) return null;
450
+ return { number: left.number / right.number, unit: right.unit ? '' : left.unit };
451
+ }
452
+ const compatible = left.unit === right.unit || left.number === 0 || right.number === 0;
453
+ if (!compatible) return null;
454
+ return { number: op === '+' ? left.number + right.number : left.number - right.number, unit: left.unit || right.unit };
455
+ };
456
+
457
+ const parsePrimary = () => {
458
+ const token = next();
459
+ if (!token) return null;
460
+ if (token.type === 'number') return parseNumber(token.raw);
461
+ if (token.type === 'var') return resolveVariable(token.name, stack);
462
+ if (token.type === 'op' && token.value === '-') {
463
+ const value = parsePrimary();
464
+ return value ? { number: -value.number, unit: value.unit } : null;
465
+ }
466
+ if (token.type === 'op' && token.value === '(') {
467
+ const value = parseExpression();
468
+ const closing = next();
469
+ return closing && closing.type === 'op' && closing.value === ')' ? value : null;
470
+ }
471
+ if (token.type === 'call') {
472
+ const args = [];
473
+ let current = parseExpression();
474
+ args.push(current);
475
+ while (peek() && peek().type === 'op' && peek().value === ',') {
476
+ next();
477
+ args.push(parseExpression());
478
+ }
479
+ const closing = next();
480
+ if (!closing || closing.type !== 'op' || closing.value !== ')') return null;
481
+ if (token.name === 'math.div' && args.length === 2) return combine(args[0], '/', args[1]);
482
+ return null;
483
+ }
484
+ return null;
485
+ };
486
+
487
+ const parseTerm = () => {
488
+ let value = parsePrimary();
489
+ while (peek() && peek().type === 'op' && (peek().value === '*' || peek().value === '/')) {
490
+ const op = next().value;
491
+ value = combine(value, op, parsePrimary());
492
+ }
493
+ return value;
494
+ };
495
+
496
+ const parseExpression = () => {
497
+ let value = parseTerm();
498
+ while (peek() && peek().type === 'op' && (peek().value === '+' || peek().value === '-')) {
499
+ const op = next().value;
500
+ value = combine(value, op, parseTerm());
501
+ }
502
+ return value;
503
+ };
504
+
505
+ const result = parseExpression();
506
+ return position === tokens.length ? result : null;
507
+ }
508
+
509
+ function formatScssValue(value) {
510
+ const rounded = Math.round(value.number * 1000) / 1000;
511
+ if (rounded === 0) return '0';
512
+ return `${rounded}${value.unit}`;
513
+ }
514
+
236
515
  function collectJsonTokens(parsed, matchesKind) {
237
516
  if (!isPlainObject(parsed)) {
238
517
  return [];
@@ -377,6 +656,34 @@ function addDefinition(definitions, {
377
656
  definitions.set(token, entry);
378
657
  }
379
658
 
659
+ const CALC_LENGTH_TIMES_VAR = /^calc\(\s*(-?[\d.]+(?:px|rem|em)?)\s*\*\s*var\([^()]*\)\s*\)$/i;
660
+ const CALC_VAR_TIMES_LENGTH = /^calc\(\s*var\([^()]*\)\s*\*\s*(-?[\d.]+(?:px|rem|em)?)\s*\)$/i;
661
+
662
+ /**
663
+ * Parse the length a token value carries. Accepts plain lengths and the
664
+ * `calc(<length> * var(--factor))` form design systems use for scaling
665
+ * (Radix Themes: `--space-1: calc(4px * var(--scaling))`).
666
+ */
667
+ function parseTokenValueLength(value) {
668
+ if (typeof value !== 'string') {
669
+ return null;
670
+ }
671
+
672
+ const trimmed = value.trim();
673
+ const direct = parseLengthToken(trimmed);
674
+ if (direct) {
675
+ return direct;
676
+ }
677
+
678
+ const match = trimmed.match(CALC_LENGTH_TIMES_VAR) || trimmed.match(CALC_VAR_TIMES_LENGTH);
679
+ if (!match) {
680
+ return null;
681
+ }
682
+
683
+ const inner = parseLengthToken(match[1]);
684
+ return inner ? { ...inner, raw: trimmed } : null;
685
+ }
686
+
380
687
  function getNormalizedValueKeys(value, baseFontSize = 16) {
381
688
  if (value === null || value === undefined) {
382
689
  return [];
@@ -388,7 +695,7 @@ function getNormalizedValueKeys(value, baseFontSize = 16) {
388
695
  }
389
696
 
390
697
  const keys = new Set([raw]);
391
- const parsed = parseLengthToken(raw);
698
+ const parsed = parseTokenValueLength(raw);
392
699
  if (!parsed) {
393
700
  return Array.from(keys);
394
701
  }
@@ -412,9 +719,11 @@ module.exports = {
412
719
  VALID_TOKEN_KINDS,
413
720
  VALID_TOKEN_SOURCE_FORMATS,
414
721
  addDefinition,
722
+ collectScssTokens,
415
723
  createTokenKindMatcher,
416
724
  getNormalizedValueKeys,
417
725
  normalizeTokenKind,
418
726
  normalizeTokenSourceFormat,
419
727
  parseTokenSources,
728
+ parseTokenValueLength,
420
729
  };
@@ -14,21 +14,18 @@ import { createAuditReport, toAuditContractReport } from 'stylelint-plugin-rhyth
14
14
  import eslintPlugin from 'stylelint-plugin-rhythmguard/eslint';
15
15
  import { getScalePreset, listScalePresetNames } from 'stylelint-plugin-rhythmguard/presets';
16
16
  import recommended from 'stylelint-plugin-rhythmguard/configs/recommended';
17
- import reactTailwind from 'stylelint-plugin-rhythmguard/configs/react-tailwind';
17
+ import embed from 'stylelint-plugin-rhythmguard/configs/embed';
18
18
  import useScale, { ruleName as useScaleName } from 'stylelint-plugin-rhythmguard/rules/use-scale';
19
19
 
20
20
  const pluginConfigs: readonly RhythmguardStylelintConfig[] = [
21
21
  plugin.configs.recommended,
22
22
  plugin.configs.strict,
23
23
  plugin.configs.tailwind,
24
- plugin.configs.expanded,
25
- plugin.configs.logical,
26
- plugin.configs.migration,
27
24
  plugin.configs.motion,
28
- plugin.configs['react-tailwind'],
29
25
  configs.recommended,
26
+ plugin.configs.embed,
27
+ embed,
30
28
  recommended,
31
- reactTailwind,
32
29
  ];
33
30
 
34
31
  const ruleOptions: RhythmguardRuleOptions = {
@@ -38,6 +35,12 @@ const ruleOptions: RhythmguardRuleOptions = {
38
35
  tokenMapFromCssCustomProperties: true,
39
36
  };
40
37
 
38
+ const autoScaleOptions: RhythmguardRuleOptions = {
39
+ scale: 'auto',
40
+ scaleSources: ['./tokens.json', { path: './theme.css', format: 'css' }],
41
+ tailwindConfigPath: './tailwind.config.mjs',
42
+ };
43
+
41
44
  const useScaleRule = rules['rhythmguard/use-scale'];
42
45
  const fixable: boolean | undefined = useScaleRule.meta.fixable;
43
46
  const docUrl: string | undefined = useScale.meta.url;
@@ -59,6 +62,7 @@ async function runAudit(): Promise<AuditContractReport> {
59
62
 
60
63
  void pluginConfigs;
61
64
  void ruleOptions;
65
+ void autoScaleOptions;
62
66
  void fixable;
63
67
  void docUrl;
64
68
  void name;
package/types/audit.d.ts CHANGED
@@ -37,7 +37,8 @@ export interface AuditOptions {
37
37
  noConfig?: boolean;
38
38
  /** Write rendered output to this file instead of stdout (equivalent to `--output`). */
39
39
  outputPath?: string;
40
- scale?: Array<number | string>;
40
+ /** Explicit scale values, or `"auto"` to infer from token sources, then scanned CSS, then the default. */
41
+ scale?: Array<number | string> | "auto";
41
42
  since?: string;
42
43
  sinceBaseline?: boolean;
43
44
  staged?: boolean;
@@ -59,7 +60,12 @@ export interface AuditSummary {
59
60
  }
60
61
 
61
62
  export interface AuditScanned {
63
+ /** Authored stylesheets scanned: .css plus .scss when postcss-scss is available. */
62
64
  cssFiles: number;
65
+ /** .scss files found. Audited through postcss-scss when it resolves. */
66
+ scssFiles?: number;
67
+ /** .scss files found but not audited because postcss-scss is not installed. */
68
+ scssSkipped?: number;
63
69
  templateFiles: number;
64
70
  totalFiles?: number;
65
71
  [key: string]: unknown;
@@ -87,8 +93,19 @@ export interface AuditBaselineComparison {
87
93
  [key: string]: unknown;
88
94
  }
89
95
 
96
+ export type AuditScaleSource = "default" | "explicit" | "fallback" | "scanned-css" | "token-package" | "token-sources";
97
+
98
+ export interface AuditScale {
99
+ /** Files the scale was derived from (token sources or scanned stylesheets). Empty for explicit, default and fallback. */
100
+ files: string[];
101
+ source: AuditScaleSource;
102
+ tokenCount: number;
103
+ values: Array<number | string>;
104
+ }
105
+
90
106
  export interface AuditReport {
91
107
  baseline?: AuditBaselineComparison | null;
108
+ scale?: AuditScale | null;
92
109
  config?: string | null;
93
110
  directory: string;
94
111
  findings: {
@@ -112,8 +129,11 @@ export interface AuditContractReport {
112
129
  motion?: unknown;
113
130
  scale: {
114
131
  cleanliness?: unknown;
132
+ files: string[];
115
133
  offScaleValues?: unknown;
134
+ source: AuditScaleSource;
116
135
  tokenOpportunities?: unknown;
136
+ values: Array<number | string> | null;
117
137
  };
118
138
  tokens?: unknown;
119
139
  };
package/types/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export type {
14
14
  RhythmguardRuleOptions,
15
15
  RhythmguardStylelintConfig,
16
16
  ScalePresetMetadata,
17
+ ScaleSource,
17
18
  ScaleValue,
18
19
  } from "./shared";
19
20
  export type {
@@ -22,6 +23,8 @@ export type {
22
23
  AuditFinding,
23
24
  AuditOptions,
24
25
  AuditReport,
26
+ AuditScale,
27
+ AuditScaleSource,
25
28
  AuditScanned,
26
29
  AuditSummary,
27
30
  AuditTokenKind,
@@ -35,11 +38,8 @@ export type { StylelintRuleModule } from "./rule";
35
38
  export interface RhythmguardPlugin extends Array<StylelintRuleModule> {
36
39
  audit: typeof auditApi;
37
40
  configs: {
38
- expanded: RhythmguardStylelintConfig;
39
- logical: RhythmguardStylelintConfig;
40
- migration: RhythmguardStylelintConfig;
41
+ embed: RhythmguardStylelintConfig;
41
42
  motion: RhythmguardStylelintConfig;
42
- "react-tailwind": RhythmguardStylelintConfig;
43
43
  recommended: RhythmguardStylelintConfig;
44
44
  strict: RhythmguardStylelintConfig;
45
45
  tailwind: RhythmguardStylelintConfig;
package/types/shared.d.ts CHANGED
@@ -2,13 +2,36 @@ export type ScaleValue = number | string;
2
2
 
3
3
  export type RuleSeverity = boolean | "always" | "never";
4
4
 
5
+ export interface ScaleSource {
6
+ /** Directory that a relative `path` resolves from. Defaults to the working directory. */
7
+ baseDir?: string;
8
+ /** Alias for `path`. */
9
+ file?: string;
10
+ /** `auto` (default), `css`, `flat-json`, `style-dictionary`, or `dtcg`. */
11
+ format?: string;
12
+ path?: string;
13
+ /** Regex for token names in this file, overriding the spacing kind matcher (for packages that name spacing differently). */
14
+ tokenPattern?: string;
15
+ }
16
+
5
17
  export interface RhythmguardRuleOptions {
18
+ /** Exempt non-zero lengths of one CSS pixel or less (1px, -1px, 0.5px, 0.0625rem). Default true. */
19
+ allowHairlines?: boolean;
6
20
  baseFontSize?: number;
7
21
  customScale?: ScaleValue[];
8
22
  includeMathFunctions?: boolean;
9
23
  preset?: string;
10
24
  properties?: Array<string | RegExp>;
11
- scale?: ScaleValue[];
25
+ /**
26
+ * Allowed values, or `"auto"` to infer the scale from spacing tokens: `scaleSources`,
27
+ * then `.rhythmguardrc.json` audit token sources, then the linted stylesheet's custom
28
+ * properties, then `tailwindConfigPath`, falling back to the `rhythmic-4` preset.
29
+ */
30
+ scale?: ScaleValue[] | "auto";
31
+ /** Token files consulted first when `scale` is `"auto"`. */
32
+ scaleSources?: Array<string | ScaleSource>;
33
+ /** Tailwind v3 config whose `theme.spacing` feeds `scale: "auto"` and `tokenMapFromTailwindSpacing`. */
34
+ tailwindConfigPath?: string;
12
35
  tokenMap?: Record<string, string>;
13
36
  tokenMapFile?: string;
14
37
  tokenMapFromCssCustomProperties?: boolean;
@@ -1,27 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- plugins: ['stylelint-plugin-rhythmguard'],
5
- rules: {
6
- 'rhythmguard/use-scale': [
7
- true,
8
- {
9
- propertyGroups: ['spacing', 'radius', 'typography', 'size'],
10
- scale: [0, 2, 4, 8, 12, 16, 24, 32, 40, 48, 64],
11
- },
12
- ],
13
- 'rhythmguard/no-offscale-transform': [
14
- true,
15
- {
16
- scale: [0, 4, 8, 12, 16, 24, 32],
17
- },
18
- ],
19
- 'rhythmguard/prefer-token': [
20
- true,
21
- {
22
- propertyGroups: ['spacing', 'radius'],
23
- tokenPattern: '^--space-|^--radius-',
24
- },
25
- ],
26
- },
27
- };
@@ -1,4 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- const require = createRequire(import.meta.url);
3
- const config = require('./expanded.js');
4
- export default config;
@@ -1,16 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- extends: [
5
- 'stylelint-plugin-logical-css/configs/recommended',
6
- 'stylelint-plugin-rhythmguard/configs/strict',
7
- ],
8
- rules: {
9
- 'rhythmguard/use-scale': [
10
- true,
11
- {
12
- propertyGroups: ['spacing', 'size'],
13
- },
14
- ],
15
- },
16
- };
@@ -1,31 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- plugins: ['stylelint-plugin-rhythmguard'],
5
- rules: {
6
- 'rhythmguard/use-scale': [
7
- true,
8
- {
9
- propertyGroups: ['spacing', 'radius'],
10
- scale: [0, 2, 4, 8, 12, 16, 24, 32, 40, 48, 64],
11
- },
12
- ],
13
- 'rhythmguard/prefer-token': [
14
- true,
15
- {
16
- allowNumericScale: true,
17
- propertyGroups: ['spacing', 'radius'],
18
- tokenMapFromCssCustomProperties: true,
19
- tokenMapFromTailwindSpacing: true,
20
- tailwindConfigPath: './tailwind.config.js',
21
- tokenPattern: '^--space-|^--radius-',
22
- },
23
- ],
24
- 'rhythmguard/no-offscale-transform': [
25
- true,
26
- {
27
- scale: [0, 4, 8, 12, 16, 24, 32],
28
- },
29
- ],
30
- },
31
- };
@@ -1,4 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- const require = createRequire(import.meta.url);
3
- const config = require('./migration.js');
4
- export default config;
@@ -1,25 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- extends: [
5
- 'stylelint-plugin-rhythmguard/configs/tailwind',
6
- ],
7
- ignoreFiles: [
8
- '.next/**',
9
- 'out/**',
10
- 'node_modules/**',
11
- ],
12
- overrides: [
13
- {
14
- files: ['**/*.module.css'],
15
- rules: {
16
- 'rhythmguard/use-scale': [
17
- true,
18
- {
19
- propertyGroups: ['spacing', 'radius'],
20
- },
21
- ],
22
- },
23
- },
24
- ],
25
- };
@@ -1,4 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- const require = createRequire(import.meta.url);
3
- const config = require('./react-tailwind.js');
4
- export default config;