@designtools/adherence 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @designtools/adherence
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 3da728f: The default exclude leaves out the `(design)` route group (`app/(design)/**`, `src/app/(design)/**`), where the skills put the docs and every other design page. A project laid out that way had its docs and copied blocks counted as product, and its headline share understated by nine points in the first end-to-end run.
8
+
9
+ - Layout is not off-system: a bracketed width, measure, grid template, aspect ratio or z-index (`max-w-[40rem]`, `grid-cols-[auto_1fr]`) is art direction the system deliberately does not tokenise. An `auto` margin (`mt-auto`) is not a spacing override.
10
+
11
+ - A status fill used as text (`text-destructive`, `text-success`, `text-warning`) is reported as off-system (`status-text`), pointing to its `-subdued-foreground` or, for a warning, neutral text with an icon.
12
+
3
13
  ## 0.1.0
4
14
 
5
15
  ### Minor Changes
package/README.md CHANGED
@@ -27,7 +27,7 @@ None beyond the manifest. It finds `components.json` from `designtools.json` at
27
27
 
28
28
  ## What it scans
29
29
 
30
- The product: `app/` and `src/`, by default. Never the system folder, the docs pages under `app/design/`, tests, `__tests__` folders, fixtures, examples, stories or `node_modules`. `--include <glob>` replaces the product globs, `--exclude <glob>` adds to what is left out; repeat either, or record them under `adherence` in `designtools.json`. A Next folder name in a glob means the folder: `--exclude "app/(app)/system/**"` leaves out that route group.
30
+ The product: `app/` and `src/`, by default. Never the system folder, the docs and design pages under `app/design/` or the `app/(design)/` route group, tests, `__tests__` folders, fixtures, examples, stories or `node_modules`. `--include <glob>` replaces the product globs, `--exclude <glob>` adds to what is left out; repeat either, or record them under `adherence` in `designtools.json`. A Next folder name in a glob means the folder: `--exclude "app/(app)/system/**"` leaves out that route group.
31
31
 
32
32
  ## What it reports
33
33
 
@@ -40,11 +40,12 @@ From these comes the share: system component uses ÷ (system component uses + ra
40
40
  | Kind | Examples |
41
41
  | --- | --- |
42
42
  | palette | `bg-blue-500`, `hover:text-primary-500/80`, `bg-(--color-primary-500)`: any `--color-<family>-<step>` ramp step in `tokens.json`, and Tailwind's default palette, used directly |
43
- | arbitrary | `p-[13px]`, `md:bg-[#fff]`, `grid-cols-[1fr_2fr]`, `[mask-type:alpha]`. A token reference in brackets, `w-[var(--size-md)]`, is not one |
43
+ | arbitrary | `p-[13px]`, `md:bg-[#fff]`, `h-[18px]`, `[mask-type:alpha]`. A token reference in brackets, `w-[var(--size-md)]`, is not one, and neither is layout the system does not tokenise: widths and measures (`w-`, `min-w-`, `max-w-`), grid templates (`grid-cols-`, `grid-rows-`, `columns-`), `aspect-` and `z-` |
44
+ | status text | `text-destructive`, `text-success`, `md:text-warning`: a status fill used as text. The fill is measured against its own label, not the page, and fails there (red on a dark page, 2.4:1). Use `-subdued-foreground`, which the generator guarantees on every background of its mode, or neutral text with an icon for a warning |
44
45
 
45
46
  They are found wherever Tailwind classes live, `className`, `cn()`, `clsx()`, `tv()`, `cva()` and the like, by [eslint-plugin-better-tailwindcss](https://github.com/schoero/eslint-plugin-better-tailwindcss)'s `no-restricted-classes` rule, run through ESLint's Node API with an in-memory config generated from `tokens.json`. The project's own ESLint setup is neither read nor changed.
46
47
 
47
- **Overrides.** A `className` on a system component that changes its colour (`bg-`, `text-`, `border-`, `ring-` … with a colour), spacing (`p-`, `m-`, `gap-`, `space-`) or radius (`rounded-`). That usually means someone fought the component instead of choosing a variant, or that the component is missing one. Classes inside `cn()` count; layout and size (`w-full`, `flex-1`, `text-sm`) do not.
48
+ **Overrides.** A `className` on a system component that changes its colour (`bg-`, `text-`, `border-`, `ring-` … with a colour), spacing (`p-`, `m-`, `gap-`, `space-`) or radius (`rounded-`). That usually means someone fought the component instead of choosing a variant, or that the component is missing one. Classes inside `cn()` count; layout and size (`w-full`, `flex-1`, `text-sm`, and an `auto` margin such as `mt-auto`) do not.
48
49
 
49
50
  All of it per file, per route and in total. A route is a Next app router page, `app/**/page.tsx`, with route groups dropped (`app/(shop)/checkout/page.tsx` is `/checkout`). A route counts its page, the files beside it, and files in folders under it that lead to no other page, such as a private `_components` folder. A layout shared by several routes counts per file only. Per component, it also counts which props are passed and which variant options are picked, and lists the components the product never uses.
50
51
 
@@ -8,6 +8,9 @@ var DEFAULT_INCLUDE = ["app/**/*.{tsx,jsx,ts,js}", "src/**/*.{tsx,jsx,ts,js}"];
8
8
  var DEFAULT_EXCLUDE = [
9
9
  "app/design/**",
10
10
  "src/app/design/**",
11
+ // the (design) route group, where the skills put the docs and every other design page
12
+ "app/(design)/**",
13
+ "src/app/(design)/**",
11
14
  "**/node_modules/**",
12
15
  "**/*.d.ts",
13
16
  "**/*.{test,spec}.*",
@@ -85,7 +88,7 @@ function formatMarkdown(report) {
85
88
  );
86
89
  lines.push(
87
90
  "",
88
- `${s.offSystemValues} off-system ${plural(s.offSystemValues, "value")} (${s.offSystem.palette} palette, ${s.offSystem.arbitrary} arbitrary) \xB7 ${s.overrides} ${plural(s.overrides, "override")} \xB7 ${s.files} ${plural(s.files, "file")} scanned, ${s.filesWithFindings} with findings.`
91
+ `${s.offSystemValues} off-system ${plural(s.offSystemValues, "value")} (${s.offSystem.palette} palette, ${s.offSystem.arbitrary} arbitrary, ${s.offSystem["status-text"] ?? 0} status text) \xB7 ${s.overrides} ${plural(s.overrides, "override")} \xB7 ${s.files} ${plural(s.files, "file")} scanned, ${s.filesWithFindings} with findings.`
89
92
  );
90
93
  lines.push("", "Warnings only: nothing here blocks a merge.");
91
94
  if (!s.replaceable.length) {
@@ -144,7 +147,7 @@ function fileBlock(f) {
144
147
  ...f.offSystem.map((v) => [
145
148
  v.line,
146
149
  v.column,
147
- `\`${v.class}\`: ${v.kind === "palette" ? "a palette step; use a semantic colour" : "an arbitrary value; use a token"}`
150
+ `\`${v.class}\`: ${OFF_SYSTEM_ADVICE[v.kind]}`
148
151
  ]),
149
152
  ...f.overrides.map((o) => [
150
153
  o.line,
@@ -170,6 +173,11 @@ function shareCell(c) {
170
173
  function plural(n, word) {
171
174
  return n === 1 ? word : `${word}s`;
172
175
  }
176
+ var OFF_SYSTEM_ADVICE = {
177
+ palette: "a palette step; use a semantic colour",
178
+ arbitrary: "an arbitrary value; use a token",
179
+ "status-text": "a status fill as text, unchecked on the page; use its `-subdued-foreground`, or neutral text with an icon for a warning"
180
+ };
173
181
 
174
182
  // src/discipline.ts
175
183
  import { existsSync as existsSync2 } from "fs";
@@ -237,6 +245,7 @@ var COLOUR_UTILITIES = [
237
245
  ];
238
246
  var esc = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
239
247
  var wrap = (utility) => `^((?:\\S*:)?!?-?${utility}!?)$`;
248
+ var LAYOUT = `(?:z|w|min-w|max-w|grid-cols|grid-rows|columns|aspect)-\\[`;
240
249
  function restrictionsFor(families) {
241
250
  const fams = [...new Set(families)].sort((a, b) => b.length - a.length || cmp(a, b)).map(esc).join("|");
242
251
  const utils = [...COLOUR_UTILITIES].sort((a, b) => b.length - a.length).map(esc).join("|");
@@ -244,8 +253,14 @@ function restrictionsFor(families) {
244
253
  const palette = `(?:${utils})-(?:${step}|\\(--color-${step}\\)|\\[var\\(--color-${step}\\)\\])(?:\\/\\S+)?`;
245
254
  return [
246
255
  { pattern: wrap(palette), message: "[designtools:palette] $1" },
247
- // a bracketed value that is not a token reference, or an arbitrary property
248
- { pattern: wrap(`(?:[a-z][\\w-]*-\\[(?!var\\(--)[^\\]]+\\](?:\\/\\S+)?|\\[[a-z-]+:[^\\]]+\\])`), message: "[designtools:arbitrary] $1" }
256
+ // a bracketed value that is not a token reference, or an arbitrary property. Layout is left
257
+ // alone: container widths, grid templates, aspect ratios and z-index are not tokens
258
+ // (mxa-tokens, "Deliberately not tokenised"), so `max-w-[40rem]` is art direction, not drift.
259
+ // a status colour's fill used as text: it is measured against its own label, not the page, and
260
+ // fails there (red text on a dark page, 2.4:1 for every brand tested). Its subdued label is the
261
+ // text tone that reads on every background; warning text is neutral text with an icon.
262
+ { pattern: wrap(`text-(?:success|warning|destructive)(?:\\/\\S+)?`), message: "[designtools:status-text] $1" },
263
+ { pattern: wrap(`(?:(?!${LAYOUT})[a-z][\\w-]*-\\[(?!var\\(--)[^\\]]+\\](?:\\/\\S+)?|\\[[a-z-]+:[^\\]]+\\])`), message: "[designtools:arbitrary] $1" }
249
264
  ];
250
265
  }
251
266
  function familiesFromTokens(tokens) {
@@ -293,7 +308,7 @@ async function checkDiscipline(root, files, families) {
293
308
  failed.push({ file, reason: m.message });
294
309
  continue;
295
310
  }
296
- const hit = /\[designtools:(palette|arbitrary)\] (\S+)/.exec(m.message);
311
+ const hit = /\[designtools:(palette|arbitrary|status-text)\] (\S+)/.exec(m.message);
297
312
  if (hit) found.push({ class: hit[2], kind: hit[1], line: m.line, column: m.column });
298
313
  }
299
314
  if (found.length) values.set(file, found.sort((a, b) => a.line - b.line || a.column - b.column || cmp(a.class, b.class)));
@@ -390,7 +405,7 @@ function findOverrides(uses, ctx) {
390
405
  function overrideKind(cls, ctx) {
391
406
  const utility = stripVariants(cls).replace(/^!/, "").replace(/!$/, "").replace(/^-/, "");
392
407
  if (RADIUS.test(utility)) return "radius";
393
- if (SPACING.test(utility)) return "spacing";
408
+ if (SPACING.test(utility) && !/-auto$/.test(utility)) return "spacing";
394
409
  for (const prefix of [...COLOUR_UTILITIES].sort((a, b) => b.length - a.length)) {
395
410
  if (!utility.startsWith(`${prefix}-`)) continue;
396
411
  const value = utility.slice(prefix.length + 1).replace(/\/[^/]+$/, "");
@@ -889,7 +904,8 @@ async function buildReport(config) {
889
904
  filesWithFindings: fileEntries.filter((f) => f.raw.length || f.offSystem.length || f.overrides.length).length,
890
905
  offSystem: {
891
906
  palette: all.filter((v) => v.kind === "palette").length,
892
- arbitrary: all.filter((v) => v.kind === "arbitrary").length
907
+ arbitrary: all.filter((v) => v.kind === "arbitrary").length,
908
+ "status-text": all.filter((v) => v.kind === "status-text").length
893
909
  },
894
910
  overrideKinds: {
895
911
  colour: overrideClasses.filter((c) => c.kind === "colour").length,
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  formatMarkdown,
7
7
  loadConfig,
8
8
  serialise
9
- } from "./chunk-AZ7JFJQ6.js";
9
+ } from "./chunk-7FM4ZQLW.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { mkdirSync, writeFileSync } from "fs";
@@ -27,7 +27,7 @@ Usage
27
27
  Options
28
28
  --manifest <file> The components.json to read (default: from designtools.json)
29
29
  --include <glob> Product files to scan; repeat for each (default: app/** and src/**)
30
- --exclude <glob> Files to leave out, on top of the system folder, app/design,
30
+ --exclude <glob> Files to leave out, on top of the system folder, app/design, app/(design),
31
31
  tests, fixtures, examples and stories; repeat for each
32
32
  --format <md|json> What to print (default: md)
33
33
  --json <file> Write the JSON report to a file as well
package/dist/index.d.ts CHANGED
@@ -162,8 +162,10 @@ interface RawElement {
162
162
  /**
163
163
  * `palette`: a primitive ramp step used directly, `bg-blue-500` or `text-primary-500`.
164
164
  * `arbitrary`: a value in brackets, `p-[13px]` or `bg-[#fff]`.
165
+ * `status-text`: a status fill used as text, `text-destructive`: use `text-destructive-subdued-foreground`,
166
+ * which reads on every background, or neutral text with an icon for a warning.
165
167
  */
166
- type OffSystemKind = "palette" | "arbitrary";
168
+ type OffSystemKind = "palette" | "arbitrary" | "status-text";
167
169
  interface OffSystemValue {
168
170
  /** The class as written, variants included: `hover:bg-blue-500`. */
169
171
  class: string;
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  routeOf,
29
29
  scanJsx,
30
30
  serialise
31
- } from "./chunk-AZ7JFJQ6.js";
31
+ } from "./chunk-7FM4ZQLW.js";
32
32
  export {
33
33
  ADHERENCE_SCHEMA,
34
34
  COLOUR_UTILITIES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designtools/adherence",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A warn-only report on how closely a React product follows its design system: system components against the raw elements they replace, classes that bypass the tokens, and classNames that override a component. Reads the @designtools/manifest JSON; markdown for a pull request, JSON for docs.",
5
5
  "type": "module",
6
6
  "license": "MIT",