@josueavalosjim/taste-check 0.7.0 → 0.8.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.
- package/README.md +8 -8
- package/package.json +1 -1
- package/src/color.mjs +59 -9
package/README.md
CHANGED
|
@@ -457,10 +457,11 @@ component are all invisible here. What this gives you is that the
|
|
|
457
457
|
values in your token file relate to each other the way you said they should. It
|
|
458
458
|
does not prove what a visitor sees.
|
|
459
459
|
|
|
460
|
-
**
|
|
461
|
-
`rgba()`, `hsl()`, `hsla()`, `hwb()`, `oklch()
|
|
462
|
-
`black` / `transparent`. `
|
|
463
|
-
A value it cannot parse fails, so you hear about it
|
|
460
|
+
**Not every colour format parses.** What does: hex in 3, 4, 6 and 8 digits;
|
|
461
|
+
`rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `oklch()`, `oklab()`, `lab()`
|
|
462
|
+
and `lch()`; and `white` / `black` / `transparent`. `color-mix()` and
|
|
463
|
+
`color()` do not. A value it cannot parse fails, so you hear about it
|
|
464
|
+
immediately.
|
|
464
465
|
|
|
465
466
|
An `oklch()` outside the sRGB gamut is clipped rather than gamut-mapped, which
|
|
466
467
|
is what a browser canvas does with it. That was checked rather than assumed:
|
|
@@ -482,10 +483,9 @@ Not built. Written down so the shape is clear.
|
|
|
482
483
|
|
|
483
484
|
**YAML configs**, once there is a reason to take on a parser.
|
|
484
485
|
|
|
485
|
-
**`
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
then check every case against a browser rather than trusting the matrices.
|
|
486
|
+
**`color-mix()` and `color()`**, which are a different shape of problem: one
|
|
487
|
+
needs an interpolation model and the other a colour space argument, so neither
|
|
488
|
+
is another conversion function.
|
|
489
489
|
|
|
490
490
|
## Development
|
|
491
491
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josueavalosjim/taste-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Design review in CI with a line down the middle: measured checks that gate the build (WCAG contrast from your tokens or from a real rendered page, one-off values in your markup) and a fresh-eyes model judge whose verdicts stay advisory. Zero dependencies. Ships no design rules of its own.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
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 = ['
|
|
28
|
+
const KNOWN_UNSUPPORTED = ['color-mix', 'color'];
|
|
29
29
|
|
|
30
30
|
const NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)%?$/;
|
|
31
31
|
|
|
@@ -94,6 +94,45 @@ function oklabToRgb(L, a, b) {
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* CIELAB to sRGB.
|
|
99
|
+
*
|
|
100
|
+
* Longer than the OKLab path because CSS specifies lab() and lch() against the
|
|
101
|
+
* D50 white point while sRGB is D65, so a chromatic adaptation sits in the
|
|
102
|
+
* middle. The matrix below folds the adaptation and the XYZ-to-sRGB step
|
|
103
|
+
* together, which is how CSS Color 4 publishes it.
|
|
104
|
+
*
|
|
105
|
+
* Every constant here is checked against a browser rather than trusted. See
|
|
106
|
+
* the lab corpus in the tests: recalling a colour matrix correctly and
|
|
107
|
+
* recalling it confidently feel identical from the inside.
|
|
108
|
+
*/
|
|
109
|
+
const D50 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
110
|
+
const XYZ_D50_TO_LINEAR_SRGB = [
|
|
111
|
+
[3.1341359569958707, -1.6173863321612538, -0.4906619460083532],
|
|
112
|
+
[-0.9787684456108496, 1.9161415707653082, 0.03344273116131949],
|
|
113
|
+
[0.07203000098937905, -0.22898208700867398, 1.4053851622747988],
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
function labToRgb(L, a, b) {
|
|
117
|
+
const e = 216 / 24389;
|
|
118
|
+
const k = 24389 / 27;
|
|
119
|
+
const fy = (L + 16) / 116;
|
|
120
|
+
const fx = a / 500 + fy;
|
|
121
|
+
const fz = fy - b / 200;
|
|
122
|
+
const xr = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
|
|
123
|
+
const yr = L > k * e ? fy ** 3 : L / k;
|
|
124
|
+
const zr = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
|
|
125
|
+
const xyz = [xr * D50[0], yr * D50[1], zr * D50[2]];
|
|
126
|
+
const linear = XYZ_D50_TO_LINEAR_SRGB.map((row) => row.reduce((sum, m, i) => sum + m * xyz[i], 0));
|
|
127
|
+
return linear.map((c) => {
|
|
128
|
+
const v =
|
|
129
|
+
c <= 0.0031308
|
|
130
|
+
? 12.92 * c
|
|
131
|
+
: 1.055 * Math.abs(c) ** (1 / 2.4) * Math.sign(c) - 0.055 * Math.sign(c);
|
|
132
|
+
return clamp(v, 0, 1) * 255;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
97
136
|
/** HSL to sRGB. h in turns, s and l in 0-1. */
|
|
98
137
|
function hslToRgb(h, s, l) {
|
|
99
138
|
const f = (n) => {
|
|
@@ -153,29 +192,39 @@ export function parseColor(input) {
|
|
|
153
192
|
const fn = text.match(/^([a-zA-Z-]+)\s*\(([\s\S]*)\)$/);
|
|
154
193
|
if (fn) {
|
|
155
194
|
const name = fn[1].toLowerCase();
|
|
156
|
-
if (name === 'oklch' || name === 'oklab') {
|
|
195
|
+
if (name === 'oklch' || name === 'oklab' || name === 'lab' || name === 'lch') {
|
|
196
|
+
const cie = name === 'lab' || name === 'lch';
|
|
197
|
+
const polarSpace = name === 'oklch' || name === 'lch';
|
|
157
198
|
const [head, ...tail] = fn[2].split('/');
|
|
158
199
|
if (tail.length > 1) return err(`"${text}" has more than one slash`);
|
|
159
200
|
const parts = head.trim().split(/[,\s]+/).filter(Boolean);
|
|
160
201
|
const alphaToken = tail.length ? tail[0].trim() : undefined;
|
|
161
202
|
if (parts.length !== 3) return err(`"${text}" needs three components`);
|
|
162
203
|
|
|
163
|
-
// Lightness
|
|
164
|
-
|
|
204
|
+
// Lightness runs 0-1 in the OK spaces and 0-100 in the CIE ones. A
|
|
205
|
+
// percentage means the same thing in both, scaled to whichever.
|
|
206
|
+
const top = cie ? 100 : 1;
|
|
207
|
+
const L = parts[0].endsWith('%')
|
|
208
|
+
? percent(parts[0]) * top
|
|
209
|
+
: NUMBER.test(parts[0])
|
|
210
|
+
? parseFloat(parts[0])
|
|
211
|
+
: null;
|
|
165
212
|
if (L === null) return err(`"${text}" has a lightness that is not a number`);
|
|
213
|
+
// What 100% means for the other two components, per CSS Color 4.
|
|
214
|
+
const abFull = cie ? 125 : 0.4;
|
|
215
|
+
const chromaFull = cie ? 150 : 0.4;
|
|
166
216
|
|
|
167
217
|
let a;
|
|
168
218
|
let b;
|
|
169
|
-
if (
|
|
170
|
-
// a and b are roughly -0.4 to 0.4; a percentage is relative to 0.4.
|
|
219
|
+
if (!polarSpace) {
|
|
171
220
|
const ab = [parts[1], parts[2]].map((t) =>
|
|
172
|
-
t.endsWith('%') ? percent(t) *
|
|
221
|
+
t.endsWith('%') ? percent(t) * abFull : NUMBER.test(t) ? parseFloat(t) : null,
|
|
173
222
|
);
|
|
174
223
|
if (ab.some((v) => v === null)) return err(`"${text}" has a component that is not a number`);
|
|
175
224
|
[a, b] = ab;
|
|
176
225
|
} else {
|
|
177
226
|
const C = parts[1].endsWith('%')
|
|
178
|
-
? percent(parts[1]) *
|
|
227
|
+
? percent(parts[1]) * chromaFull
|
|
179
228
|
: NUMBER.test(parts[1])
|
|
180
229
|
? parseFloat(parts[1])
|
|
181
230
|
: null;
|
|
@@ -193,7 +242,8 @@ export function parseColor(input) {
|
|
|
193
242
|
if (parsed === null) return err(`"${text}" has an alpha that is not a number`);
|
|
194
243
|
alphaValue = parsed;
|
|
195
244
|
}
|
|
196
|
-
|
|
245
|
+
const rgb = cie ? labToRgb(clamp(L, 0, 100), a, b) : oklabToRgb(clamp(L, 0, 1), a, b);
|
|
246
|
+
return ok([...rgb, clamp(alphaValue, 0, 1)]);
|
|
197
247
|
}
|
|
198
248
|
|
|
199
249
|
const polar = name === 'hsl' || name === 'hsla' || name === 'hwb';
|