@josueavalosjim/taste-check 0.3.1 → 0.4.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/README.md +95 -26
- package/package.json +3 -2
- package/src/color.mjs +73 -1
package/README.md
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
# taste-check
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
contrast ratios from your own custom properties, and flags class names and
|
|
5
|
-
literal values that are not on your own approved list.
|
|
3
|
+
Design review in CI, with a line down the middle of it.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Some of design review is measurable. Contrast ratios, values that should have
|
|
6
|
+
been tokens, class names nobody approved. taste-check measures those and fails
|
|
7
|
+
your build on them.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
The rest is judgment, and it cannot be measured. For that it sends your
|
|
10
|
+
screenshots and your checklist to a model, and reports what comes back as an
|
|
11
|
+
opinion rather than a result.
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
Keeping those two apart is the whole design. A tool that blurs them either
|
|
14
|
+
blocks your pipeline on a coin flip or quietly downgrades a real failure to a
|
|
15
|
+
suggestion.
|
|
15
16
|
|
|
16
17
|
```bash
|
|
17
18
|
npm i -D @josueavalosjim/taste-check
|
|
18
19
|
```
|
|
19
20
|
|
|
21
|
+
```bash
|
|
22
|
+
npx @josueavalosjim/taste-check
|
|
23
|
+
```
|
|
24
|
+
|
|
20
25
|
```
|
|
21
26
|
contrast ok, 10 pairs across 2 themes
|
|
22
27
|
ok 17.76:1 needs 4.5 light --text-strong on --surface body text
|
|
@@ -28,20 +33,37 @@ treatments FAILED
|
|
|
28
33
|
FAIL src/Promo.jsx:8 inline value "#ff0055" on <a> is a one-off. Use a token, or add it to approvedValues.
|
|
29
34
|
```
|
|
30
35
|
|
|
36
|
+
```bash
|
|
37
|
+
npx @josueavalosjim/taste-check judge
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
judge ok, 5 lines against 2 screenshots, 1 to read
|
|
42
|
+
NOTE fail Nothing important is cut off at the edge: the third card runs
|
|
43
|
+
past the right edge of the frame at roughly x=798 of 800.
|
|
44
|
+
```
|
|
45
|
+
|
|
31
46
|
Zero runtime dependencies. Node 22 or newer.
|
|
32
47
|
|
|
33
48
|
## Why this exists
|
|
34
49
|
|
|
35
|
-
|
|
36
|
-
an element fails 4.5:1
|
|
37
|
-
while your captions answer to 4.5:1
|
|
38
|
-
spec's.
|
|
50
|
+
Linters have a gate and no taste. They enforce what someone could write down
|
|
51
|
+
as a rule, which is why they can tell you an element fails 4.5:1 but not that
|
|
52
|
+
your borders answer to 3:1 while your captions answer to 4.5:1. That
|
|
53
|
+
distinction is yours, not the spec's.
|
|
54
|
+
|
|
55
|
+
The newer AI design reviewers have taste and no gate. They will tell you what
|
|
56
|
+
is wrong with a screen, in prose, in a chat window, and some of them arrive
|
|
57
|
+
with a few hundred opinions already loaded about what good looks like.
|
|
39
58
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
Both halves are useful and they need to stay separable. Run the same model
|
|
60
|
+
twice over the same unchanged screen and you can get two different answers,
|
|
61
|
+
which is fine for advice and disqualifying for a build gate. So the measured
|
|
62
|
+
half here gates, the judged half does not, and the tool will not let you
|
|
63
|
+
confuse one for the other by accident.
|
|
64
|
+
|
|
65
|
+
It also ships no design rules of its own. Not a palette, not a class list, not
|
|
66
|
+
a contrast floor, not a checklist. You supply all of it.
|
|
45
67
|
|
|
46
68
|
## What counts as a failure
|
|
47
69
|
|
|
@@ -58,6 +80,12 @@ Each of these exits 1 rather than passing quietly:
|
|
|
58
80
|
A check that cannot fail is worse than no check, because it goes green and gets
|
|
59
81
|
quoted as evidence.
|
|
60
82
|
|
|
83
|
+
The judge is held to the same rule from the other side. Its verdicts never
|
|
84
|
+
affect the exit code by default, but a judge that could not run does: no
|
|
85
|
+
screenshots, a command that died, a reply that was not JSON or that skipped a
|
|
86
|
+
checklist line. "Did not run" and "found nothing" must not print the same
|
|
87
|
+
thing.
|
|
88
|
+
|
|
61
89
|
## The contrast check
|
|
62
90
|
|
|
63
91
|
Give it your custom properties, describe your themes, and list what must clear
|
|
@@ -252,6 +280,33 @@ taste-check judge [options] Ask a fresh-eyes judge about your screenshots
|
|
|
252
280
|
Exit code is 1 if any check fails, 0 if every check ran and passed. The judge
|
|
253
281
|
plays by the rules in its own section above.
|
|
254
282
|
|
|
283
|
+
## Where it sits next to other tools
|
|
284
|
+
|
|
285
|
+
This is a small tool with a narrow claim, and several of these are better than
|
|
286
|
+
it at the thing they do. Reach for them.
|
|
287
|
+
|
|
288
|
+
**axe, pa11y.** They run against a real rendered page and catch far more than
|
|
289
|
+
contrast. taste-check checks pairs you declare in a config, before a page
|
|
290
|
+
exists and without a browser. Use both. If you only run one accessibility
|
|
291
|
+
tool, run axe.
|
|
292
|
+
|
|
293
|
+
**stylelint, eslint.** General code quality, with an enormous rule ecosystem.
|
|
294
|
+
Nothing here replaces them.
|
|
295
|
+
|
|
296
|
+
**@lapidist/design-lint.** More thorough than taste-check on the token and
|
|
297
|
+
component side: it parses properly rather than scanning, knows about
|
|
298
|
+
frameworks, autofixes, and manages deprecations. If enforcing tokens in code is
|
|
299
|
+
the whole of your problem, it is the better fit.
|
|
300
|
+
|
|
301
|
+
**Checklist Design and similar agent skills.** They arrive with a hundred or
|
|
302
|
+
more published checklists and review conversationally. If you want good
|
|
303
|
+
opinions supplied, take theirs. taste-check supplies none on purpose and runs
|
|
304
|
+
in CI with an exit code instead.
|
|
305
|
+
|
|
306
|
+
What is left, and the reason this exists: nothing above draws a line between
|
|
307
|
+
the part that can gate a build and the part that cannot. The linters have no
|
|
308
|
+
judgment, the judges have no gate.
|
|
309
|
+
|
|
255
310
|
## What this does not do
|
|
256
311
|
|
|
257
312
|
Read this before trusting a green run.
|
|
@@ -265,10 +320,15 @@ values in your token file relate to each other the way you said they should. It
|
|
|
265
320
|
does not prove what a visitor sees.
|
|
266
321
|
|
|
267
322
|
**Only some colour formats parse.** Hex in 3, 4, 6 and 8 digits; `rgb()`,
|
|
268
|
-
`rgba()`, `hsl()`, `hsla()
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
323
|
+
`rgba()`, `hsl()`, `hsla()`, `hwb()`, `oklch()` and `oklab()`; and `white` /
|
|
324
|
+
`black` / `transparent`. `lab()`, `lch()` and `color-mix()` are not parsed yet.
|
|
325
|
+
A value it cannot parse fails, so you hear about it immediately.
|
|
326
|
+
|
|
327
|
+
An `oklch()` outside the sRGB gamut is clipped rather than gamut-mapped, which
|
|
328
|
+
is what a browser canvas does with it. That was checked rather than assumed:
|
|
329
|
+
the test corpus has fifty deliberately out-of-gamut colours painted in a real
|
|
330
|
+
browser and read back as pixels, and the parser agrees with all of them to
|
|
331
|
+
within one channel unit.
|
|
272
332
|
|
|
273
333
|
**There is no specificity resolution.** Scopes apply in the order you list
|
|
274
334
|
them. If your tokens rely on `.a.b` beating `.b`, list the scopes in the order
|
|
@@ -283,13 +343,22 @@ invisible to it.
|
|
|
283
343
|
Not built. Written down so the shape is clear.
|
|
284
344
|
|
|
285
345
|
**A runtime mode**, closing the gap named above by measuring `getComputedStyle`
|
|
286
|
-
in a real browser, as an optional peer dependency so the core stays free of
|
|
346
|
+
in a real browser, as an optional peer dependency so the core stays free of
|
|
347
|
+
one. This is the one that matters most. An advisory judge is only worth
|
|
348
|
+
listening to if the measured half beside it is genuinely measured, and reading
|
|
349
|
+
a token file is the weaker version of that.
|
|
287
350
|
|
|
288
351
|
**YAML configs**, once there is a reason to take on a parser.
|
|
289
352
|
|
|
290
|
-
**
|
|
291
|
-
|
|
292
|
-
|
|
353
|
+
**SARIF output**, so findings land in a code scanning tab rather than only in
|
|
354
|
+
a log.
|
|
355
|
+
|
|
356
|
+
**A way to run the judge from an agent skill**, not only from a shell.
|
|
357
|
+
|
|
358
|
+
**`lab()` and `lch()`**, which need the D50 white point and a chromatic
|
|
359
|
+
adaptation step that `oklch()` does not. Completeness rather than reach, so
|
|
360
|
+
it sits behind the others. Worth doing the same way when it happens: derive it,
|
|
361
|
+
then check every case against a browser rather than trusting the matrices.
|
|
293
362
|
|
|
294
363
|
## Development
|
|
295
364
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josueavalosjim/taste-check",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Design review in CI with a line down the middle: measured checks that gate the build (WCAG contrast over your own tokens, one-off values in your own markup) and a fresh-eyes model judge whose verdicts stay advisory. Ships no design rules of its own.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
7
7
|
"cli",
|
|
8
8
|
"contrast",
|
|
9
|
+
"design-review",
|
|
9
10
|
"design-system",
|
|
10
11
|
"design-tokens",
|
|
11
12
|
"lint",
|
package/src/color.mjs
CHANGED
|
@@ -25,7 +25,7 @@ const NAMED = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
/** Formats deliberately not supported in v1, named so the error is useful. */
|
|
28
|
-
const KNOWN_UNSUPPORTED = ['lab', 'lch', '
|
|
28
|
+
const KNOWN_UNSUPPORTED = ['lab', 'lch', 'color-mix', 'color'];
|
|
29
29
|
|
|
30
30
|
const NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)%?$/;
|
|
31
31
|
|
|
@@ -65,6 +65,35 @@ function percent(token) {
|
|
|
65
65
|
return parseFloat(token) / 100;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* OKLab to sRGB, via linear sRGB.
|
|
70
|
+
*
|
|
71
|
+
* The matrices are Björn Ottosson's, which is what CSS Color 4 specifies.
|
|
72
|
+
* They are checked against a browser rather than trusted: see the oklch corpus
|
|
73
|
+
* in the tests, generated by painting each colour on a canvas and reading the
|
|
74
|
+
* pixel back.
|
|
75
|
+
*/
|
|
76
|
+
function oklabToRgb(L, a, b) {
|
|
77
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
78
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
79
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
80
|
+
const l = l_ * l_ * l_;
|
|
81
|
+
const m = m_ * m_ * m_;
|
|
82
|
+
const q = s_ * s_ * s_;
|
|
83
|
+
const linear = [
|
|
84
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * q,
|
|
85
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * q,
|
|
86
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * q,
|
|
87
|
+
];
|
|
88
|
+
// Linear to gamma-encoded sRGB, then clipped into gamut. Clipping is the
|
|
89
|
+
// simple choice and it is not what a browser does for wildly out-of-gamut
|
|
90
|
+
// colours, which is why parseColor reports whether it had to clip.
|
|
91
|
+
return linear.map((c) => {
|
|
92
|
+
const v = c <= 0.0031308 ? 12.92 * c : 1.055 * Math.abs(c) ** (1 / 2.4) * Math.sign(c) - 0.055 * Math.sign(c);
|
|
93
|
+
return clamp(v, 0, 1) * 255;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
/** HSL to sRGB. h in turns, s and l in 0-1. */
|
|
69
98
|
function hslToRgb(h, s, l) {
|
|
70
99
|
const f = (n) => {
|
|
@@ -124,6 +153,49 @@ export function parseColor(input) {
|
|
|
124
153
|
const fn = text.match(/^([a-zA-Z-]+)\s*\(([\s\S]*)\)$/);
|
|
125
154
|
if (fn) {
|
|
126
155
|
const name = fn[1].toLowerCase();
|
|
156
|
+
if (name === 'oklch' || name === 'oklab') {
|
|
157
|
+
const [head, ...tail] = fn[2].split('/');
|
|
158
|
+
if (tail.length > 1) return err(`"${text}" has more than one slash`);
|
|
159
|
+
const parts = head.trim().split(/[,\s]+/).filter(Boolean);
|
|
160
|
+
const alphaToken = tail.length ? tail[0].trim() : undefined;
|
|
161
|
+
if (parts.length !== 3) return err(`"${text}" needs three components`);
|
|
162
|
+
|
|
163
|
+
// Lightness is 0-1, or a percentage of that.
|
|
164
|
+
const L = parts[0].endsWith('%') ? percent(parts[0]) : NUMBER.test(parts[0]) ? parseFloat(parts[0]) : null;
|
|
165
|
+
if (L === null) return err(`"${text}" has a lightness that is not a number`);
|
|
166
|
+
|
|
167
|
+
let a;
|
|
168
|
+
let b;
|
|
169
|
+
if (name === 'oklab') {
|
|
170
|
+
// a and b are roughly -0.4 to 0.4; a percentage is relative to 0.4.
|
|
171
|
+
const ab = [parts[1], parts[2]].map((t) =>
|
|
172
|
+
t.endsWith('%') ? percent(t) * 0.4 : NUMBER.test(t) ? parseFloat(t) : null,
|
|
173
|
+
);
|
|
174
|
+
if (ab.some((v) => v === null)) return err(`"${text}" has a component that is not a number`);
|
|
175
|
+
[a, b] = ab;
|
|
176
|
+
} else {
|
|
177
|
+
const C = parts[1].endsWith('%')
|
|
178
|
+
? percent(parts[1]) * 0.4
|
|
179
|
+
: NUMBER.test(parts[1])
|
|
180
|
+
? parseFloat(parts[1])
|
|
181
|
+
: null;
|
|
182
|
+
if (C === null) return err(`"${text}" has a chroma that is not a number`);
|
|
183
|
+
const h = hue(parts[2]);
|
|
184
|
+
if (h === null) return err(`"${text}" has a hue that is not an angle`);
|
|
185
|
+
const radians = h * 2 * Math.PI;
|
|
186
|
+
a = Math.max(C, 0) * Math.cos(radians);
|
|
187
|
+
b = Math.max(C, 0) * Math.sin(radians);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let alphaValue = 1;
|
|
191
|
+
if (alphaToken !== undefined) {
|
|
192
|
+
const parsed = alpha(alphaToken);
|
|
193
|
+
if (parsed === null) return err(`"${text}" has an alpha that is not a number`);
|
|
194
|
+
alphaValue = parsed;
|
|
195
|
+
}
|
|
196
|
+
return ok([...oklabToRgb(clamp(L, 0, 1), a, b), clamp(alphaValue, 0, 1)]);
|
|
197
|
+
}
|
|
198
|
+
|
|
127
199
|
const polar = name === 'hsl' || name === 'hsla' || name === 'hwb';
|
|
128
200
|
if (polar) {
|
|
129
201
|
// Same two syntaxes as rgb(): comma separated, or space separated with a
|