@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 +10 -0
- package/README.md +4 -3
- package/dist/{chunk-AZ7JFJQ6.js → chunk-7FM4ZQLW.js} +23 -7
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
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
|
|
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]`, `
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@designtools/adherence",
|
|
3
|
-
"version": "0.1.
|
|
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",
|