@josueavalosjim/taste-check 0.3.1 → 0.4.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 +12 -7
- package/package.json +1 -1
- package/src/color.mjs +73 -1
package/README.md
CHANGED
|
@@ -265,10 +265,15 @@ values in your token file relate to each other the way you said they should. It
|
|
|
265
265
|
does not prove what a visitor sees.
|
|
266
266
|
|
|
267
267
|
**Only some colour formats parse.** Hex in 3, 4, 6 and 8 digits; `rgb()`,
|
|
268
|
-
`rgba()`, `hsl()`, `hsla()
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
`rgba()`, `hsl()`, `hsla()`, `hwb()`, `oklch()` and `oklab()`; and `white` /
|
|
269
|
+
`black` / `transparent`. `lab()`, `lch()` and `color-mix()` are not parsed yet.
|
|
270
|
+
A value it cannot parse fails, so you hear about it immediately.
|
|
271
|
+
|
|
272
|
+
An `oklch()` outside the sRGB gamut is clipped rather than gamut-mapped, which
|
|
273
|
+
is what a browser canvas does with it. That was checked rather than assumed:
|
|
274
|
+
the test corpus has fifty deliberately out-of-gamut colours painted in a real
|
|
275
|
+
browser and read back as pixels, and the parser agrees with all of them to
|
|
276
|
+
within one channel unit.
|
|
272
277
|
|
|
273
278
|
**There is no specificity resolution.** Scopes apply in the order you list
|
|
274
279
|
them. If your tokens rely on `.a.b` beating `.b`, list the scopes in the order
|
|
@@ -287,9 +292,9 @@ in a real browser, as an optional peer dependency so the core stays free of one.
|
|
|
287
292
|
|
|
288
293
|
**YAML configs**, once there is a reason to take on a parser.
|
|
289
294
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
295
|
+
**`lab()` and `lch()`**, which need the D50 white point and a chromatic
|
|
296
|
+
adaptation step that `oklch()` does not. Worth doing the same way: derive it,
|
|
297
|
+
then check every case against a browser rather than trusting the matrices.
|
|
293
298
|
|
|
294
299
|
## Development
|
|
295
300
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josueavalosjim/taste-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Deterministic design-system checks: WCAG contrast over your own tokens, and a one-off value linter over your own approved list. No palette, no class list, no opinions shipped.",
|
|
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 = ['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
|