@instruments/materia-ui 0.4.0 → 0.4.2

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 (117) hide show
  1. package/README.md +2 -4
  2. package/agent-docs.md +9 -9
  3. package/dist/components/accordion/accordion-root.d.ts +1 -0
  4. package/dist/components/accordion/accordion-root.js +1 -0
  5. package/dist/components/alert/alert-root.d.ts +1 -0
  6. package/dist/components/alert/alert-root.js +1 -0
  7. package/dist/components/alert-dialog/alert-dialog-root.d.ts +1 -0
  8. package/dist/components/alert-dialog/alert-dialog-root.js +1 -0
  9. package/dist/components/avatar/avatar-root.d.ts +1 -0
  10. package/dist/components/avatar/avatar-root.js +1 -0
  11. package/dist/components/badge/badge-root.d.ts +1 -0
  12. package/dist/components/badge/badge-root.js +1 -0
  13. package/dist/components/calendar/calendar-root.js +1 -0
  14. package/dist/components/carousel/carousel-root.d.ts +1 -0
  15. package/dist/components/carousel/carousel-root.js +1 -0
  16. package/dist/components/chart/chart-root.d.ts +1 -0
  17. package/dist/components/chart/chart-root.js +1 -0
  18. package/dist/components/collapsible/collapsible-root.d.ts +1 -0
  19. package/dist/components/collapsible/collapsible-root.js +1 -0
  20. package/dist/components/combobox/combobox-root.d.ts +1 -0
  21. package/dist/components/combobox/combobox-root.js +1 -0
  22. package/dist/components/command/command-dialog.d.ts +5 -1
  23. package/dist/components/command/command-dialog.js +1 -1
  24. package/dist/components/command/command-input.js +5 -2
  25. package/dist/components/command/command-root.d.ts +1 -0
  26. package/dist/components/command/command-root.js +1 -0
  27. package/dist/components/dialog/dialog-content.d.ts +2 -0
  28. package/dist/components/dialog/dialog-content.js +3 -2
  29. package/dist/components/dialog/dialog-footer.d.ts +2 -0
  30. package/dist/components/dialog/dialog-footer.js +8 -8
  31. package/dist/components/dialog/dialog-root.d.ts +1 -0
  32. package/dist/components/dialog/dialog-root.js +1 -0
  33. package/dist/components/dialog/dialog-title.d.ts +2 -0
  34. package/dist/components/dialog/dialog-title.js +3 -1
  35. package/dist/components/drawer/drawer-root.d.ts +1 -0
  36. package/dist/components/drawer/drawer-root.js +1 -0
  37. package/dist/components/heading/heading-root.d.ts +1 -0
  38. package/dist/components/heading/heading-root.js +1 -0
  39. package/dist/components/input/input-root.d.ts +1 -0
  40. package/dist/components/input/input-root.js +1 -0
  41. package/dist/components/kbd/kbd-root.d.ts +1 -0
  42. package/dist/components/kbd/kbd-root.js +1 -0
  43. package/dist/components/logo/logo-mark.d.ts +1 -1
  44. package/dist/components/logo/logo-mark.js +1 -1
  45. package/dist/components/logotype/logotype-paths.d.ts +1 -1
  46. package/dist/components/logotype/logotype-paths.js +1 -1
  47. package/dist/components/month-year-picker/month-year-picker-root.d.ts +1 -0
  48. package/dist/components/month-year-picker/month-year-picker-root.js +1 -0
  49. package/dist/components/native-select/native-select-root.d.ts +1 -0
  50. package/dist/components/native-select/native-select-root.js +1 -0
  51. package/dist/components/pill/pill-root.d.ts +1 -0
  52. package/dist/components/pill/pill-root.js +1 -0
  53. package/dist/components/popover/popover-root.d.ts +1 -0
  54. package/dist/components/popover/popover-root.js +1 -0
  55. package/dist/components/quantity-control/quantity-control-root.d.ts +1 -0
  56. package/dist/components/quantity-control/quantity-control-root.js +1 -0
  57. package/dist/components/segmented-control/segmented-control-root.js +6 -3
  58. package/dist/components/segmented-control/segmented-control-variants.js +4 -1
  59. package/dist/components/select/select-root.d.ts +1 -0
  60. package/dist/components/select/select-root.js +1 -0
  61. package/dist/components/separator/separator-root.d.ts +1 -0
  62. package/dist/components/separator/separator-root.js +1 -0
  63. package/dist/components/sheet/sheet-root.d.ts +1 -0
  64. package/dist/components/sheet/sheet-root.js +1 -0
  65. package/dist/components/spinner/spinner-root.d.ts +1 -0
  66. package/dist/components/spinner/spinner-root.js +1 -0
  67. package/dist/components/stepper/stepper-root.d.ts +1 -0
  68. package/dist/components/stepper/stepper-root.js +1 -0
  69. package/dist/components/success-tick-ripple/success-tick-ripple-root.d.ts +1 -0
  70. package/dist/components/success-tick-ripple/success-tick-ripple-root.js +1 -0
  71. package/dist/components/swatch-selector/swatch-selector-item.js +4 -1
  72. package/dist/components/switch/switch-root.d.ts +1 -0
  73. package/dist/components/switch/switch-root.js +1 -0
  74. package/dist/components/tag/tag-root.d.ts +1 -0
  75. package/dist/components/tag/tag-root.js +1 -0
  76. package/dist/components/textarea/textarea-root.d.ts +1 -0
  77. package/dist/components/textarea/textarea-root.js +1 -0
  78. package/dist/components/time-slot-picker/time-slot-picker-root.d.ts +1 -0
  79. package/dist/components/time-slot-picker/time-slot-picker-root.js +1 -0
  80. package/dist/components/toast/toast-root.d.ts +1 -0
  81. package/dist/components/toast/toast-root.js +1 -0
  82. package/dist/components/tooltip/tooltip-root.d.ts +1 -0
  83. package/dist/components/tooltip/tooltip-root.js +1 -0
  84. package/dist/compositions/empty-state/empty-state-root.d.ts +1 -0
  85. package/dist/compositions/empty-state/empty-state-root.js +1 -0
  86. package/dist/compositions/file-upload/file-upload-dropzone.d.ts +1 -0
  87. package/dist/compositions/file-upload/file-upload-dropzone.js +1 -0
  88. package/dist/data/decision-trees.d.ts +1 -1
  89. package/dist/data/decision-trees.js +1 -1
  90. package/dist/data/docs-manifest.d.ts +2 -0
  91. package/dist/data/docs-manifest.js +235 -68
  92. package/dist/styles/index.css +0 -1
  93. package/dist/tokens/segmented.d.ts +8 -0
  94. package/dist/tokens/segmented.js +26 -0
  95. package/dist/utils/cn-theme.js +1 -1
  96. package/lint/README.md +6 -2
  97. package/lint/doctrine-rules.mjs +4 -4
  98. package/lint/oxlint/index.d.mts +13 -5
  99. package/lint/oxlint/index.mjs +26 -0
  100. package/lint/oxlint/no-naked-element.mjs +123 -65
  101. package/lint/oxlint/no-render-layout-as.mjs +61 -56
  102. package/lint/scan-classes.d.mts +58 -5
  103. package/lint/scan-classes.mjs +274 -38
  104. package/lint/tokens.d.mts +16 -8
  105. package/lint/tokens.mjs +13 -3
  106. package/lint/type-case.d.mts +16 -8
  107. package/lint/type-case.mjs +17 -0
  108. package/package.json +27 -14
  109. package/lint/oxlint/fixtures/allowed.tsx +0 -12
  110. package/lint/oxlint/fixtures/fixture.config.mjs +0 -29
  111. package/lint/oxlint/fixtures/invalid.tsx +0 -24
  112. package/lint/oxlint/fixtures/layout-only.tsx +0 -26
  113. package/lint/oxlint/fixtures/render-as-invalid.tsx +0 -11
  114. package/lint/oxlint/fixtures/render-as-valid.tsx +0 -14
  115. package/lint/oxlint/fixtures/valid.tsx +0 -18
  116. package/lint/oxlint/no-naked-element.test.ts +0 -155
  117. package/lint/oxlint/no-render-layout-as.test.ts +0 -81
@@ -1,5 +1,58 @@
1
- /**
2
- * The class-string scanner is a command, run as the `materia-ui-lint` bin. It
3
- * exports nothing; importing it runs the scan.
4
- */
5
- export {};
1
+ // GENERATED by scripts/generate-lint-types.ts from the JSDoc in lint/scan-classes.mjs - do not edit.
2
+ // Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-types`.
3
+
4
+ export interface Options {
5
+ /**
6
+ * Files and directories to scan.
7
+ */
8
+ paths: string[];
9
+ /**
10
+ * Path fragments to skip.
11
+ */
12
+ excludes: string[];
13
+ /**
14
+ * Whether the `hex` rule is on.
15
+ */
16
+ hex: boolean;
17
+ /**
18
+ * Whether the `arbitrary` rule is on.
19
+ */
20
+ arbitrary: boolean;
21
+ /**
22
+ * Whether to print a JSON document instead of text lines.
23
+ */
24
+ json: boolean;
25
+ /**
26
+ * Whether to print usage and stop.
27
+ */
28
+ help: boolean;
29
+ }
30
+ export type RuleName = "hex" | "arbitrary";
31
+ export interface Region {
32
+ /**
33
+ * Offset of the region in the file.
34
+ */
35
+ start: number;
36
+ /**
37
+ * The region's text.
38
+ */
39
+ body: string;
40
+ }
41
+ export interface Violation {
42
+ /**
43
+ * The rule that matched.
44
+ */
45
+ rule: RuleName;
46
+ /**
47
+ * The offending text.
48
+ */
49
+ value: string;
50
+ /**
51
+ * One-based line of the match.
52
+ */
53
+ line: number;
54
+ /**
55
+ * One-based column of the match.
56
+ */
57
+ column: number;
58
+ }
@@ -15,7 +15,15 @@
15
15
  * @doctrine Class strings carry no raw hex/oklch/oklab colours and no arbitrary Tailwind values - only design tokens and theme utilities.
16
16
  *
17
17
  * Usage:
18
- * node scan-classes.mjs <path...> [--arbitrary] [--all] [--no-hex] [--exclude <substr>]
18
+ * materia-ui-lint <path...> [--arbitrary] [--all] [--no-hex]
19
+ * [--exclude <substr>] [--json]
20
+ * materia-ui-lint --help
21
+ *
22
+ * Exit codes:
23
+ * 0 no violations (or --help)
24
+ * 1 violations found
25
+ * 2 usage error (unknown flag, missing value, no paths)
26
+ * 3 a path could not be read
19
27
  */
20
28
 
21
29
  import { readFileSync, readdirSync, statSync } from "node:fs";
@@ -36,14 +44,106 @@ const HEX_PATTERN = /#[0-9a-fA-F]{3,8}\b/gu;
36
44
  const COLOR_FUNCTION = /\bokl(?:ch|ab)\(/giu;
37
45
  const ARBITRARY_PATTERN = /[a-z][a-z0-9]*(?:-[a-z0-9]+)*-\[[^\]]+\]/gu;
38
46
 
47
+ const EXIT_VIOLATIONS = 1;
48
+ const EXIT_USAGE = 2;
49
+ const EXIT_IO = 3;
50
+
51
+ /** @type {Record<RuleName, string>} */
52
+ const MESSAGES = {
53
+ hex: "raw hex/oklch colour in class string - use a design token",
54
+ arbitrary: "arbitrary Tailwind value - use a token or theme utility",
55
+ };
56
+
57
+ const USAGE = `Usage: materia-ui-lint <path...> [options]
58
+
59
+ Scan class strings for raw colours and arbitrary Tailwind values.
60
+ Files (.ts .tsx .js .jsx .mjs .cjs) are found by walking each path.
61
+
62
+ Options:
63
+ --arbitrary Also flag arbitrary Tailwind values such as mt-[13px]
64
+ --all Turn on every rule (hex and arbitrary)
65
+ --no-hex Turn the hex rule off
66
+ --exclude <substr> Skip paths containing <substr> (repeatable;
67
+ --exclude=<substr> also works)
68
+ --json Print one JSON document instead of text lines:
69
+ { "violations": [{ file, line, column, rule, value,
70
+ message }], "rules": [...], "files": <count> }
71
+ -h, --help Show this help
72
+
73
+ Exit codes:
74
+ 0 no violations
75
+ 1 violations found
76
+ 2 usage error
77
+ 3 a path could not be read
78
+ `;
79
+
80
+ /** A failure that ends the run with a message and a specific exit code. */
81
+ class ScanError extends Error {
82
+ /**
83
+ * @param {string} message What to print to stderr.
84
+ * @param {number} exitCode The process exit code.
85
+ * @param {ErrorOptions} [options] The underlying cause, if any.
86
+ */
87
+ constructor(message, exitCode, options) {
88
+ super(message, options);
89
+ this.name = "ScanError";
90
+ this.exitCode = exitCode;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * @param {string} message What was wrong with the command line.
96
+ * @returns {ScanError} An exit-2 error that points at `--help`.
97
+ */
98
+ const usageError = (message) =>
99
+ new ScanError(`${message} (see --help)`, EXIT_USAGE);
100
+
101
+ /**
102
+ * @typedef {object} Options
103
+ * @property {string[]} paths Files and directories to scan.
104
+ * @property {string[]} excludes Path fragments to skip.
105
+ * @property {boolean} hex Whether the `hex` rule is on.
106
+ * @property {boolean} arbitrary Whether the `arbitrary` rule is on.
107
+ * @property {boolean} json Whether to print a JSON document instead of text lines.
108
+ * @property {boolean} help Whether to print usage and stop.
109
+ */
110
+
111
+ /** @typedef {"hex" | "arbitrary"} RuleName */
112
+
113
+ /**
114
+ * @typedef {object} Region
115
+ * @property {number} start Offset of the region in the file.
116
+ * @property {string} body The region's text.
117
+ */
118
+
119
+ /**
120
+ * @typedef {object} Violation
121
+ * @property {RuleName} rule The rule that matched.
122
+ * @property {string} value The offending text.
123
+ * @property {number} line One-based line of the match.
124
+ * @property {number} column One-based column of the match.
125
+ */
126
+
127
+ /**
128
+ * @param {string[]} argv The command-line arguments after the script name.
129
+ * @returns {Options} What to scan and which rules are on.
130
+ * @throws {ScanError} On an unknown flag or an `--exclude` without a value.
131
+ */
39
132
  const parseArgs = (argv) => {
133
+ /** @type {string[]} */
40
134
  const paths = [];
135
+ /** @type {string[]} */
41
136
  const excludes = [];
42
137
  let hex = true;
43
138
  let arbitrary = false;
139
+ let json = false;
44
140
 
45
- for (let index = 0; index < argv.length; index += 1) {
46
- const arg = argv[index];
141
+ // `--exclude <substr>` takes the next argument, so walk an iterator.
142
+ const args = argv.values();
143
+ for (const arg of args) {
144
+ if (arg === "--help" || arg === "-h") {
145
+ return { paths, excludes, hex, arbitrary, json, help: true };
146
+ }
47
147
  if (arg === "--no-hex") {
48
148
  hex = false;
49
149
  } else if (arg === "--arbitrary") {
@@ -51,32 +151,94 @@ const parseArgs = (argv) => {
51
151
  } else if (arg === "--all") {
52
152
  hex = true;
53
153
  arbitrary = true;
154
+ } else if (arg === "--json") {
155
+ json = true;
54
156
  } else if (arg === "--exclude") {
55
- index += 1;
56
- if (argv[index] !== undefined) {
57
- excludes.push(argv[index]);
157
+ const next = args.next();
158
+ if (next.done === true || next.value.startsWith("-")) {
159
+ throw usageError("--exclude needs a value");
58
160
  }
161
+ excludes.push(next.value);
59
162
  } else if (arg.startsWith("--exclude=")) {
60
- excludes.push(arg.slice("--exclude=".length));
61
- } else if (!arg.startsWith("--")) {
163
+ const value = arg.slice("--exclude=".length);
164
+ if (value === "") {
165
+ throw usageError("--exclude needs a value");
166
+ }
167
+ excludes.push(value);
168
+ } else if (arg.startsWith("-")) {
169
+ throw usageError(`unknown flag ${arg}`);
170
+ } else {
62
171
  paths.push(arg);
63
172
  }
64
173
  }
65
174
 
66
- return { paths, excludes, hex, arbitrary };
175
+ return { paths, excludes, hex, arbitrary, json, help: false };
176
+ };
177
+
178
+ /**
179
+ * @param {unknown} error Whatever a filesystem call threw.
180
+ * @returns {string} Its error code, or its message when it has none.
181
+ */
182
+ const describeFsError = (error) => {
183
+ if (error instanceof Error && "code" in error) {
184
+ return String(error.code);
185
+ }
186
+ return error instanceof Error ? error.message : String(error);
187
+ };
188
+
189
+ /**
190
+ * Run a filesystem call, naming the path if it fails.
191
+ *
192
+ * @template T
193
+ * @param {string} target The path the call touches.
194
+ * @param {() => T} action The filesystem call.
195
+ * @returns {T} What the call returned.
196
+ * @throws {ScanError} When the call throws.
197
+ */
198
+ const reading = (target, action) => {
199
+ try {
200
+ return action();
201
+ } catch (error) {
202
+ throw new ScanError(
203
+ `cannot read ${target} (${describeFsError(error)})`,
204
+ EXIT_IO,
205
+ { cause: error }
206
+ );
207
+ }
67
208
  };
68
209
 
69
- const walk = (target, excludes, out) => {
70
- if (excludes.some((fragment) => target.includes(fragment))) {
210
+ /**
211
+ * @param {string[]} fragments Path fragments to skip.
212
+ * @returns {RegExp | undefined} A pattern matching any of them, if there are any.
213
+ */
214
+ const excludePattern = (fragments) =>
215
+ fragments.length === 0
216
+ ? undefined
217
+ : new RegExp(
218
+ fragments
219
+ .map((fragment) =>
220
+ fragment.replaceAll(/[$()*+.?[\\\]^{|}]/gu, String.raw`\$&`)
221
+ )
222
+ .join("|"),
223
+ "u"
224
+ );
225
+
226
+ /**
227
+ * @param {string} target A file or directory.
228
+ * @param {RegExp | undefined} excluded Matches the paths to skip.
229
+ * @param {string[]} out Collects the scannable files.
230
+ */
231
+ const walk = (target, excluded, out) => {
232
+ if (excluded?.test(target) === true) {
71
233
  return;
72
234
  }
73
- const stats = statSync(target);
235
+ const stats = reading(target, () => statSync(target));
74
236
  if (stats.isDirectory()) {
75
- for (const entry of readdirSync(target)) {
237
+ for (const entry of reading(target, () => readdirSync(target))) {
76
238
  if (entry === "node_modules" || entry.startsWith(".")) {
77
239
  continue;
78
240
  }
79
- walk(path.join(target, entry), excludes, out);
241
+ walk(path.join(target, entry), excluded, out);
80
242
  }
81
243
  return;
82
244
  }
@@ -86,6 +248,12 @@ const walk = (target, excludes, out) => {
86
248
  }
87
249
  };
88
250
 
251
+ /**
252
+ * @param {string} text The file text.
253
+ * @param {number} openIndex Offset of the opening quote.
254
+ * @param {string} quote The quote character that closes the string.
255
+ * @returns {number} Offset of the closing quote, or the last offset if unclosed.
256
+ */
89
257
  const findStringEnd = (text, openIndex, quote) => {
90
258
  for (let index = openIndex + 1; index < text.length; index += 1) {
91
259
  const char = text[index];
@@ -100,6 +268,13 @@ const findStringEnd = (text, openIndex, quote) => {
100
268
  return text.length - 1;
101
269
  };
102
270
 
271
+ /**
272
+ * @param {string} text The file text.
273
+ * @param {number} openIndex Offset of the opening bracket.
274
+ * @param {string} open The opening bracket character.
275
+ * @param {string} close The closing bracket character.
276
+ * @returns {number} Offset of the matching close, or the last offset if unbalanced.
277
+ */
103
278
  const findMatching = (text, openIndex, open, close) => {
104
279
  let depth = 0;
105
280
  for (let index = openIndex; index < text.length; index += 1) {
@@ -121,8 +296,12 @@ const findMatching = (text, openIndex, open, close) => {
121
296
  * attribute values plus the argument lists of the common class-composition
122
297
  * helpers. Scanning only these ranges keeps hex detection off inline styles and
123
298
  * ordinary colour constants.
299
+ *
300
+ * @param {string} text The file text.
301
+ * @returns {Region[]} The class-string ranges, in source order within each kind.
124
302
  */
125
303
  const extractClassRegions = (text) => {
304
+ /** @type {Region[]} */
126
305
  const regions = [];
127
306
 
128
307
  CLASS_ATTRIBUTE.lastIndex = 0;
@@ -159,6 +338,11 @@ const extractClassRegions = (text) => {
159
338
  return regions;
160
339
  };
161
340
 
341
+ /**
342
+ * @param {string} text The file text.
343
+ * @param {number} offset A character offset into it.
344
+ * @returns {{ line: number, column: number }} The one-based line and column.
345
+ */
162
346
  const lineColumnAt = (text, offset) => {
163
347
  let line = 1;
164
348
  let lineStart = 0;
@@ -171,6 +355,14 @@ const lineColumnAt = (text, offset) => {
171
355
  return { line, column: offset - lineStart + 1 };
172
356
  };
173
357
 
358
+ /**
359
+ * @param {Region} region The class-string range to search.
360
+ * @param {RegExp} pattern The global pattern to match.
361
+ * @param {RuleName} rule The rule a match counts against.
362
+ * @param {string} text The whole file, for line and column.
363
+ * @param {Set<string>} seen Offset and rule pairs already reported.
364
+ * @param {Violation[]} violations Collects the matches.
365
+ */
174
366
  const collectMatches = (region, pattern, rule, text, seen, violations) => {
175
367
  pattern.lastIndex = 0;
176
368
  let match = pattern.exec(region.body);
@@ -186,10 +378,17 @@ const collectMatches = (region, pattern, rule, text, seen, violations) => {
186
378
  }
187
379
  };
188
380
 
381
+ /**
382
+ * @param {string} file The file to scan.
383
+ * @param {{ hex: boolean, arbitrary: boolean }} rules Which rules are on.
384
+ * @returns {Violation[]} The violations, ordered by position.
385
+ */
189
386
  const scanFile = (file, rules) => {
190
- const text = readFileSync(file, "utf-8");
387
+ const text = reading(file, () => readFileSync(file, "utf-8"));
191
388
  const regions = extractClassRegions(text);
389
+ /** @type {Violation[]} */
192
390
  const violations = [];
391
+ /** @type {Set<string>} */
193
392
  const seen = new Set();
194
393
 
195
394
  for (const region of regions) {
@@ -213,42 +412,79 @@ const scanFile = (file, rules) => {
213
412
  return violations;
214
413
  };
215
414
 
216
- const main = () => {
217
- const { paths, excludes, hex, arbitrary } = parseArgs(process.argv.slice(2));
415
+ /**
416
+ * @param {Options} options The parsed command line.
417
+ * @returns {number} The exit code.
418
+ * @throws {ScanError} When no paths are given or a path cannot be read.
419
+ */
420
+ const run = (options) => {
421
+ const { paths, excludes, hex, arbitrary, json } = options;
218
422
  if (paths.length === 0) {
219
- process.stderr.write("scan-classes: no paths supplied\n");
220
- process.exit(2);
423
+ throw usageError("no paths supplied");
221
424
  }
222
425
 
426
+ /** @type {string[]} */
223
427
  const files = [];
428
+ const excluded = excludePattern(excludes);
224
429
  for (const target of paths) {
225
- walk(target, excludes, files);
430
+ walk(target, excluded, files);
431
+ }
432
+
433
+ /** @type {RuleName[]} */
434
+ const rules = [];
435
+ if (hex) {
436
+ rules.push("hex");
437
+ }
438
+ if (arbitrary) {
439
+ rules.push("arbitrary");
226
440
  }
227
441
 
228
- const messages = {
229
- hex: "raw hex/oklch colour in class string - use a design token",
230
- arbitrary: "arbitrary Tailwind value - use a token or theme utility",
231
- };
442
+ const reported = files.flatMap((file) =>
443
+ scanFile(file, { hex, arbitrary }).map((violation) => ({
444
+ file,
445
+ line: violation.line,
446
+ column: violation.column,
447
+ rule: violation.rule,
448
+ value: violation.value,
449
+ message: MESSAGES[violation.rule],
450
+ }))
451
+ );
232
452
 
233
- let total = 0;
234
- for (const file of files) {
235
- const violations = scanFile(file, { hex, arbitrary });
236
- for (const violation of violations) {
237
- total += 1;
453
+ if (json) {
454
+ process.stdout.write(
455
+ `${JSON.stringify({ violations: reported, rules, files: files.length })}\n`
456
+ );
457
+ } else {
458
+ for (const v of reported) {
238
459
  process.stdout.write(
239
- `${file}:${violation.line}:${violation.column} ${violation.rule} ${violation.value} ${messages[violation.rule]}\n`
460
+ `${v.file}:${v.line}:${v.column} ${v.rule} ${v.value} ${v.message}\n`
461
+ );
462
+ }
463
+ if (reported.length > 0) {
464
+ process.stderr.write(
465
+ `\nscan-classes: ${reported.length} violation(s) [rules: ${rules.join(", ")}]\n`
240
466
  );
241
467
  }
242
468
  }
243
469
 
244
- if (total > 0) {
245
- const active = [hex ? "hex" : null, arbitrary ? "arbitrary" : null]
246
- .filter(Boolean)
247
- .join(", ");
248
- process.stderr.write(
249
- `\nscan-classes: ${total} violation(s) [rules: ${active}]\n`
250
- );
251
- process.exit(1);
470
+ return reported.length > 0 ? EXIT_VIOLATIONS : 0;
471
+ };
472
+
473
+ const main = () => {
474
+ try {
475
+ const options = parseArgs(process.argv.slice(2));
476
+ if (options.help) {
477
+ process.stdout.write(USAGE);
478
+ return;
479
+ }
480
+ // Set rather than `process.exit()` so piped output is flushed first.
481
+ process.exitCode = run(options);
482
+ } catch (error) {
483
+ if (!(error instanceof ScanError)) {
484
+ throw error;
485
+ }
486
+ process.stderr.write(`scan-classes: ${error.message}\n`);
487
+ process.exitCode = error.exitCode;
252
488
  }
253
489
  };
254
490
 
package/lint/tokens.d.mts CHANGED
@@ -1,16 +1,24 @@
1
- /** Every `--color-<name>` the stylesheet declares, as `<name>`. */
2
- export const colorTokens: readonly string[];
1
+ // GENERATED by scripts/generate-lint-types.ts from the JSDoc in lint/tokens.mjs - do not edit.
2
+ // Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-types`.
3
3
 
4
- /** Every `--text-<name>` type role the stylesheet declares, as `<name>`. */
4
+ /**
5
+ * Every `--color-<name>` the stylesheet declares, as `<name>`.
6
+ */
7
+ export const colorTokens: readonly string[];
8
+ /**
9
+ * Every `--text-<name>` type role the stylesheet declares, as `<name>`.
10
+ */
5
11
  export const textRoles: readonly string[];
6
-
7
12
  /**
8
- * `shadcn/no-raw-colors` with Materia's tokens allowed: any utility on a
9
- * declared colour and every `text-<role>`. Spread it into an oxlint `rules`.
13
+ * `shadcn/no-raw-colors` with Materia's tokens allowed: any utility on a declared
14
+ * colour (`bg-primary`, `border-muted-foreground`) and every `text-<role>`,
15
+ * which the rule would otherwise read as an undeclared colour.
10
16
  */
11
17
  export const rawColorsRule: {
12
- readonly "shadcn/no-raw-colors": readonly [
18
+ readonly "shadcn/no-raw-colors": [
13
19
  "error",
14
- { readonly allow: readonly string[] },
20
+ {
21
+ readonly allow: readonly string[];
22
+ },
15
23
  ];
16
24
  };
package/lint/tokens.mjs CHANGED
@@ -1,11 +1,15 @@
1
- // GENERATED by scripts/generate-lint-tokens.mjs from src/styles/index.css - do not edit.
1
+ // GENERATED by scripts/generate-lint-tokens.ts from src/styles/index.css - do not edit.
2
2
  // Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-tokens`.
3
3
  //
4
4
  // Published as `@instruments/materia-ui/lint/tokens`. `shadcn/no-raw-colors`
5
5
  // skips stylesheets under node_modules, so it cannot see the tokens this package
6
6
  // declares; spread `rawColorsRule` into an app's oxlint `rules` to allow them.
7
7
 
8
- /** Every `--color-<name>` the stylesheet declares, as `<name>`. */
8
+ /**
9
+ * Every `--color-<name>` the stylesheet declares, as `<name>`.
10
+ *
11
+ * @type {readonly string[]}
12
+ */
9
13
  export const colorTokens = [
10
14
  "accent",
11
15
  "accent-foreground",
@@ -95,7 +99,11 @@ export const colorTokens = [
95
99
  "warning-soft",
96
100
  ];
97
101
 
98
- /** Every `--text-<name>` type role the stylesheet declares, as `<name>`. */
102
+ /**
103
+ * Every `--text-<name>` type role the stylesheet declares, as `<name>`.
104
+ *
105
+ * @type {readonly string[]}
106
+ */
99
107
  export const textRoles = [
100
108
  "2xs",
101
109
  "calendar",
@@ -117,6 +125,8 @@ export const textRoles = [
117
125
  * `shadcn/no-raw-colors` with Materia's tokens allowed: any utility on a declared
118
126
  * colour (`bg-primary`, `border-muted-foreground`) and every `text-<role>`,
119
127
  * which the rule would otherwise read as an undeclared colour.
128
+ *
129
+ * @type {{ readonly "shadcn/no-raw-colors": ["error", { readonly allow: readonly string[] }] }}
120
130
  */
121
131
  export const rawColorsRule = {
122
132
  "shadcn/no-raw-colors": [
@@ -1,12 +1,20 @@
1
- /** The `no-raw-type-utilities` options: the modifiers allowed beside a type role. */
2
- export interface TypeCase {
3
- readonly allow: readonly string[];
4
- }
1
+ // GENERATED by scripts/generate-lint-types.ts from the JSDoc in lint/type-case.mjs - do not edit.
2
+ // Regenerate with `pnpm --filter @instruments/materia-ui generate:lint-types`.
5
3
 
6
- /** The modifiers that may sit beside a role: weights, mono, italic and the like. */
4
+ /**
5
+ * The modifiers that may sit beside a role: weights, mono, italic and the like.
6
+ */
7
7
  export const typeCase: TypeCase;
8
-
9
- /** `howells/no-raw-type-utilities` with the type case. Spread it into an oxlint `rules`. */
8
+ /**
9
+ * `howells/no-raw-type-utilities` with the type case. Spread it into an oxlint
10
+ * `rules`.
11
+ */
10
12
  export const typeCaseRule: {
11
- readonly "howells/no-raw-type-utilities": readonly ["error", TypeCase];
13
+ readonly "howells/no-raw-type-utilities": ["error", TypeCase];
12
14
  };
15
+ /**
16
+ * The `no-raw-type-utilities` options: the modifiers allowed beside a type role.
17
+ */
18
+ export interface TypeCase {
19
+ readonly allow: readonly string[];
20
+ }
@@ -15,6 +15,17 @@
15
15
  //
16
16
  // @doctrine Text takes its size, leading and tracking from a role on the type ramp (text-caption to text-title) or the Text and Heading size props, never from raw Tailwind typography such as text-sm, leading-* or tracking-*.
17
17
 
18
+ /**
19
+ * The `no-raw-type-utilities` options: the modifiers allowed beside a type role.
20
+ *
21
+ * @typedef {{ readonly allow: readonly string[] }} TypeCase
22
+ */
23
+
24
+ /**
25
+ * The modifiers that may sit beside a role: weights, mono, italic and the like.
26
+ *
27
+ * @type {TypeCase}
28
+ */
18
29
  export const typeCase = {
19
30
  allow: [
20
31
  "font-normal",
@@ -28,6 +39,12 @@ export const typeCase = {
28
39
  ],
29
40
  };
30
41
 
42
+ /**
43
+ * `howells/no-raw-type-utilities` with the type case. Spread it into an oxlint
44
+ * `rules`.
45
+ *
46
+ * @type {{ readonly "howells/no-raw-type-utilities": ["error", TypeCase] }}
47
+ */
31
48
  export const typeCaseRule = {
32
49
  "howells/no-raw-type-utilities": ["error", typeCase],
33
50
  };