@stnd/lab 1.4.2 → 1.5.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/CHANGELOG.md +11 -0
- package/Lab.svelte +653 -167
- package/color.js +365 -0
- package/color.test.js +82 -0
- package/package.json +5 -2
package/color.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @stnd/lab — OKLCH & CSS Color Utilities
|
|
3
|
+
*
|
|
4
|
+
* Perceptual OKLCH <-> sRGB math (Björn Ottosson) and DOM color resolution
|
|
5
|
+
* so the Design Lab color picker always reflects the true color of the page
|
|
6
|
+
* and edits natively in OKLCH or Hex.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Clamp a number to [min, max].
|
|
11
|
+
* @param {number} v
|
|
12
|
+
* @param {number} min
|
|
13
|
+
* @param {number} max
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
export function clamp(v, min, max) {
|
|
17
|
+
return Math.max(min, Math.min(max, v));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert an sRGB channel (0..1) to linear-light value.
|
|
22
|
+
* @param {number} v
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function srgbToLinear(v) {
|
|
26
|
+
v = Number(v) || 0;
|
|
27
|
+
if (v <= 0.04045) return v / 12.92;
|
|
28
|
+
return Math.pow((v + 0.055) / 1.055, 2.4);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert a linear-light channel to sRGB (0..1).
|
|
33
|
+
* @param {number} v
|
|
34
|
+
* @returns {number}
|
|
35
|
+
*/
|
|
36
|
+
export function linearToSrgb(v) {
|
|
37
|
+
v = Number(v) || 0;
|
|
38
|
+
if (v <= 0.0031308) return v * 12.92;
|
|
39
|
+
return 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert sRGB (0..255) to OKLCH { L: 0..1, C: 0..0.4, h: 0..360 }.
|
|
44
|
+
* @param {number} r
|
|
45
|
+
* @param {number} g
|
|
46
|
+
* @param {number} b
|
|
47
|
+
* @returns {{ L: number, C: number, h: number }}
|
|
48
|
+
*/
|
|
49
|
+
export function rgbToOklch(r, g, b) {
|
|
50
|
+
const R = srgbToLinear(clamp(r / 255, 0, 1));
|
|
51
|
+
const G = srgbToLinear(clamp(g / 255, 0, 1));
|
|
52
|
+
const B = srgbToLinear(clamp(b / 255, 0, 1));
|
|
53
|
+
|
|
54
|
+
const l_ = 0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B;
|
|
55
|
+
const m_ = 0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B;
|
|
56
|
+
const s_ = 0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B;
|
|
57
|
+
|
|
58
|
+
const l = Math.cbrt(l_);
|
|
59
|
+
const m = Math.cbrt(m_);
|
|
60
|
+
const s = Math.cbrt(s_);
|
|
61
|
+
|
|
62
|
+
let L = clamp(0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 0, 1);
|
|
63
|
+
if (Math.abs(L - 1) < 1e-5) L = 1;
|
|
64
|
+
if (L < 1e-5) L = 0;
|
|
65
|
+
|
|
66
|
+
const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
|
67
|
+
const b_ = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
|
68
|
+
|
|
69
|
+
let C = Math.sqrt(a * a + b_ * b_);
|
|
70
|
+
if (C < 1e-4 || (r === g && g === b)) {
|
|
71
|
+
C = 0;
|
|
72
|
+
}
|
|
73
|
+
let h = C === 0 ? 0 : Math.atan2(b_, a) * (180 / Math.PI);
|
|
74
|
+
if (h < 0) h += 360;
|
|
75
|
+
|
|
76
|
+
return { L, C, h };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert OKLCH { L: 0..1, C: >=0, h: 0..360 } to sRGB { r, g, b, inGamut }.
|
|
81
|
+
* @param {number} L
|
|
82
|
+
* @param {number} C
|
|
83
|
+
* @param {number} h
|
|
84
|
+
* @returns {{ r: number, g: number, b: number, inGamut: boolean }}
|
|
85
|
+
*/
|
|
86
|
+
export function oklchToRgb(L, C, h) {
|
|
87
|
+
const rad = ((Number(h) || 0) * Math.PI) / 180;
|
|
88
|
+
const a = (Number(C) || 0) * Math.cos(rad);
|
|
89
|
+
const b = (Number(C) || 0) * Math.sin(rad);
|
|
90
|
+
|
|
91
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
92
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
93
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
94
|
+
|
|
95
|
+
const l = l_ * l_ * l_;
|
|
96
|
+
const m = m_ * m_ * m_;
|
|
97
|
+
const s = s_ * s_ * s_;
|
|
98
|
+
|
|
99
|
+
const linR = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
|
100
|
+
const linG = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
|
101
|
+
const linB = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
|
|
102
|
+
|
|
103
|
+
const eps = 0.001;
|
|
104
|
+
const inGamut =
|
|
105
|
+
linR >= -eps &&
|
|
106
|
+
linR <= 1 + eps &&
|
|
107
|
+
linG >= -eps &&
|
|
108
|
+
linG <= 1 + eps &&
|
|
109
|
+
linB >= -eps &&
|
|
110
|
+
linB <= 1 + eps;
|
|
111
|
+
|
|
112
|
+
const R = linearToSrgb(clamp(linR, 0, 1));
|
|
113
|
+
const G = linearToSrgb(clamp(linG, 0, 1));
|
|
114
|
+
const B = linearToSrgb(clamp(linB, 0, 1));
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
r: clamp(Math.round(R * 255), 0, 255),
|
|
118
|
+
g: clamp(Math.round(G * 255), 0, 255),
|
|
119
|
+
b: clamp(Math.round(B * 255), 0, 255),
|
|
120
|
+
inGamut,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Format an RGB object as #rrggbb.
|
|
126
|
+
* @param {{ r: number, g: number, b: number }} rgb
|
|
127
|
+
* @returns {string}
|
|
128
|
+
*/
|
|
129
|
+
export function rgbToHex({ r, g, b }) {
|
|
130
|
+
const hr = clamp(Math.round(r), 0, 255).toString(16).padStart(2, "0");
|
|
131
|
+
const hg = clamp(Math.round(g), 0, 255).toString(16).padStart(2, "0");
|
|
132
|
+
const hb = clamp(Math.round(b), 0, 255).toString(16).padStart(2, "0");
|
|
133
|
+
return `#${hr}${hg}${hb}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format OKLCH components into a clean CSS `oklch(L C H)` string.
|
|
138
|
+
* @param {number} L
|
|
139
|
+
* @param {number} C
|
|
140
|
+
* @param {number} h
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
export function formatOklch(L, C, h) {
|
|
144
|
+
const lStr = Number(clamp(L, 0, 1).toFixed(4));
|
|
145
|
+
const cStr = Number(clamp(C, 0, 0.4).toFixed(4));
|
|
146
|
+
const hStr = Number((((h % 360) + 360) % 360).toFixed(1));
|
|
147
|
+
return `oklch(${lStr} ${cStr} ${hStr})`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse a direct `#hex` string (`#rgb`, `#rrggbb`, `#rrggbbaa`) into `{ r, g, b }` or `null`.
|
|
152
|
+
* @param {string} str
|
|
153
|
+
* @returns {{ r: number, g: number, b: number } | null}
|
|
154
|
+
*/
|
|
155
|
+
export function parseHexString(str) {
|
|
156
|
+
if (!str || typeof str !== "string") return null;
|
|
157
|
+
let hex = str.trim();
|
|
158
|
+
if (!hex.startsWith("#")) return null;
|
|
159
|
+
hex = hex.slice(1);
|
|
160
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
161
|
+
hex = hex
|
|
162
|
+
.slice(0, 3)
|
|
163
|
+
.split("")
|
|
164
|
+
.map((c) => c + c)
|
|
165
|
+
.join("");
|
|
166
|
+
} else if (hex.length === 8) {
|
|
167
|
+
hex = hex.slice(0, 6);
|
|
168
|
+
}
|
|
169
|
+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
170
|
+
const n = parseInt(hex, 16);
|
|
171
|
+
return {
|
|
172
|
+
r: (n >> 16) & 255,
|
|
173
|
+
g: (n >> 8) & 255,
|
|
174
|
+
b: n & 255,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse a literal `oklch(L C H)` string (where L, C, H are numbers/percentages, not `from ...`)
|
|
180
|
+
* into `{ L, C, h }` or `null`.
|
|
181
|
+
* @param {string} str
|
|
182
|
+
* @returns {{ L: number, C: number, h: number } | null}
|
|
183
|
+
*/
|
|
184
|
+
export function parseOklchString(str) {
|
|
185
|
+
if (!str || typeof str !== "string") return null;
|
|
186
|
+
const m = str
|
|
187
|
+
.trim()
|
|
188
|
+
.match(
|
|
189
|
+
/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)(%?)\s+([\d.]+|none)(?:deg|rad|turn)?(?:\s*\/\s*[\d.]+%?)?\s*\)$/i,
|
|
190
|
+
);
|
|
191
|
+
if (!m) return null;
|
|
192
|
+
let L = parseFloat(m[1]);
|
|
193
|
+
if (m[2] === "%") L /= 100;
|
|
194
|
+
let C = parseFloat(m[3]);
|
|
195
|
+
if (m[4] === "%") C = (C / 100) * 0.4;
|
|
196
|
+
let h = m[5].toLowerCase() === "none" ? 0 : parseFloat(m[5]);
|
|
197
|
+
if (Number.isNaN(L) || Number.isNaN(C) || Number.isNaN(h)) return null;
|
|
198
|
+
return {
|
|
199
|
+
L: clamp(L, 0, 1),
|
|
200
|
+
C: clamp(C, 0, 0.4),
|
|
201
|
+
h: Number((((h % 360) + 360) % 360).toFixed(4)),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Parse a literal `rgb(...)` or `rgba(...)` string into `{ r, g, b }` or `null`.
|
|
207
|
+
* @param {string} str
|
|
208
|
+
* @returns {{ r: number, g: number, b: number } | null}
|
|
209
|
+
*/
|
|
210
|
+
export function parseRgbString(str) {
|
|
211
|
+
if (!str || typeof str !== "string") return null;
|
|
212
|
+
const m = str
|
|
213
|
+
.trim()
|
|
214
|
+
.match(
|
|
215
|
+
/^rgba?\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)\s*[,\s]\s*([\d.]+)(?:\s*[,/]\s*[\d.]+%?)?\s*\)$/i,
|
|
216
|
+
);
|
|
217
|
+
if (!m) return null;
|
|
218
|
+
return {
|
|
219
|
+
r: clamp(Math.round(parseFloat(m[1])), 0, 255),
|
|
220
|
+
g: clamp(Math.round(parseFloat(m[2])), 0, 255),
|
|
221
|
+
b: clamp(Math.round(parseFloat(m[3])), 0, 255),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parse any CSS color string (including `oklch(...)`, `#hex`, `rgb(...)`, named colors,
|
|
227
|
+
* or relative `oklch(from ...)` in the DOM) into `{ L, C, h, hex, rgb, css }`.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} css
|
|
230
|
+
* @param {HTMLElement} [contextEl]
|
|
231
|
+
* @returns {{ L: number, C: number, h: number, hex: string, rgb: { r: number, g: number, b: number }, css: string }}
|
|
232
|
+
*/
|
|
233
|
+
export function parseCssColor(css, contextEl) {
|
|
234
|
+
const raw = (css || "").trim();
|
|
235
|
+
if (!raw || raw.toLowerCase() === "black") {
|
|
236
|
+
return {
|
|
237
|
+
L: 0,
|
|
238
|
+
C: 0,
|
|
239
|
+
h: 0,
|
|
240
|
+
hex: "#000000",
|
|
241
|
+
rgb: { r: 0, g: 0, b: 0 },
|
|
242
|
+
css: raw || "#000000",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (raw.toLowerCase() === "white") {
|
|
246
|
+
return {
|
|
247
|
+
L: 1,
|
|
248
|
+
C: 0,
|
|
249
|
+
h: 0,
|
|
250
|
+
hex: "#ffffff",
|
|
251
|
+
rgb: { r: 255, g: 255, b: 255 },
|
|
252
|
+
css: "#ffffff",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 1. Direct oklch(L C H) string — preserves exact perceptual L, C, H
|
|
257
|
+
const directOklch = parseOklchString(raw);
|
|
258
|
+
if (directOklch) {
|
|
259
|
+
const rgb = oklchToRgb(directOklch.L, directOklch.C, directOklch.h);
|
|
260
|
+
const hex = rgbToHex(rgb);
|
|
261
|
+
return {
|
|
262
|
+
...directOklch,
|
|
263
|
+
rgb,
|
|
264
|
+
hex,
|
|
265
|
+
css: formatOklch(directOklch.L, directOklch.C, directOklch.h),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 2. Direct #hex string
|
|
270
|
+
const directHex = parseHexString(raw);
|
|
271
|
+
if (directHex) {
|
|
272
|
+
const oklch = rgbToOklch(directHex.r, directHex.g, directHex.b);
|
|
273
|
+
const hex = rgbToHex(directHex);
|
|
274
|
+
return {
|
|
275
|
+
...oklch,
|
|
276
|
+
rgb: directHex,
|
|
277
|
+
hex,
|
|
278
|
+
css: hex,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 3. Direct rgb(...) string
|
|
283
|
+
const directRgb = parseRgbString(raw);
|
|
284
|
+
if (directRgb) {
|
|
285
|
+
const oklch = rgbToOklch(directRgb.r, directRgb.g, directRgb.b);
|
|
286
|
+
const hex = rgbToHex(directRgb);
|
|
287
|
+
return {
|
|
288
|
+
...oklch,
|
|
289
|
+
rgb: directRgb,
|
|
290
|
+
hex,
|
|
291
|
+
css: formatOklch(oklch.L, oklch.C, oklch.h),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 4. Resolve via DOM & Canvas for relative colors (`oklch(from ...)`), `var(...)`, `light-dark(...)`, or named colors
|
|
296
|
+
if (typeof document !== "undefined" && document.body) {
|
|
297
|
+
try {
|
|
298
|
+
const parent = contextEl || document.body;
|
|
299
|
+
const probe = document.createElement("span");
|
|
300
|
+
probe.style.display = "none";
|
|
301
|
+
probe.style.color = raw;
|
|
302
|
+
parent.appendChild(probe);
|
|
303
|
+
const resolved = getComputedStyle(probe).color.trim();
|
|
304
|
+
probe.remove();
|
|
305
|
+
|
|
306
|
+
const resolvedOklch = parseOklchString(resolved);
|
|
307
|
+
if (resolvedOklch) {
|
|
308
|
+
const rgb = oklchToRgb(
|
|
309
|
+
resolvedOklch.L,
|
|
310
|
+
resolvedOklch.C,
|
|
311
|
+
resolvedOklch.h,
|
|
312
|
+
);
|
|
313
|
+
const hex = rgbToHex(rgb);
|
|
314
|
+
return {
|
|
315
|
+
...resolvedOklch,
|
|
316
|
+
rgb,
|
|
317
|
+
hex,
|
|
318
|
+
css: formatOklch(resolvedOklch.L, resolvedOklch.C, resolvedOklch.h),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const resolvedRgb = parseRgbString(resolved);
|
|
323
|
+
if (resolvedRgb) {
|
|
324
|
+
const oklch = rgbToOklch(resolvedRgb.r, resolvedRgb.g, resolvedRgb.b);
|
|
325
|
+
const hex = rgbToHex(resolvedRgb);
|
|
326
|
+
return {
|
|
327
|
+
...oklch,
|
|
328
|
+
rgb: resolvedRgb,
|
|
329
|
+
hex,
|
|
330
|
+
css: formatOklch(oklch.L, oklch.C, oklch.h),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fallback: 1x1 canvas pixel readback for color(srgb ...), oklab(...), etc.
|
|
335
|
+
const canvas = document.createElement("canvas");
|
|
336
|
+
canvas.width = 1;
|
|
337
|
+
canvas.height = 1;
|
|
338
|
+
const ctx = canvas.getContext("2d");
|
|
339
|
+
if (ctx && resolved) {
|
|
340
|
+
ctx.fillStyle = resolved;
|
|
341
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
342
|
+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
343
|
+
const oklch = rgbToOklch(r, g, b);
|
|
344
|
+
const hex = rgbToHex({ r, g, b });
|
|
345
|
+
return {
|
|
346
|
+
...oklch,
|
|
347
|
+
rgb: { r, g, b },
|
|
348
|
+
hex,
|
|
349
|
+
css: formatOklch(oklch.L, oklch.C, oklch.h),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
/* noop */
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
L: 0,
|
|
359
|
+
C: 0,
|
|
360
|
+
h: 0,
|
|
361
|
+
hex: "#000000",
|
|
362
|
+
rgb: { r: 0, g: 0, b: 0 },
|
|
363
|
+
css: "#000000",
|
|
364
|
+
};
|
|
365
|
+
}
|
package/color.test.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
clamp,
|
|
5
|
+
srgbToLinear,
|
|
6
|
+
linearToSrgb,
|
|
7
|
+
rgbToOklch,
|
|
8
|
+
oklchToRgb,
|
|
9
|
+
rgbToHex,
|
|
10
|
+
formatOklch,
|
|
11
|
+
parseHexString,
|
|
12
|
+
parseOklchString,
|
|
13
|
+
parseRgbString,
|
|
14
|
+
parseCssColor,
|
|
15
|
+
} from "./color.js";
|
|
16
|
+
|
|
17
|
+
test("clamp constraints", () => {
|
|
18
|
+
assert.equal(clamp(5, 0, 10), 5);
|
|
19
|
+
assert.equal(clamp(-2, 0, 10), 0);
|
|
20
|
+
assert.equal(clamp(15, 0, 10), 10);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("linear and sRGB roundtrip", () => {
|
|
24
|
+
for (const v of [0, 0.25, 0.5, 0.75, 1]) {
|
|
25
|
+
const lin = srgbToLinear(v);
|
|
26
|
+
const srgb = linearToSrgb(lin);
|
|
27
|
+
assert.ok(Math.abs(srgb - v) < 1e-4, `Expected ${v}, got ${srgb}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("rgbToOklch and oklchToRgb roundtrip", () => {
|
|
32
|
+
// Test white
|
|
33
|
+
const whiteOklch = rgbToOklch(255, 255, 255);
|
|
34
|
+
assert.ok(Math.abs(whiteOklch.L - 1) < 1e-3);
|
|
35
|
+
assert.ok(whiteOklch.C < 1e-3);
|
|
36
|
+
|
|
37
|
+
// Test black
|
|
38
|
+
const blackOklch = rgbToOklch(0, 0, 0);
|
|
39
|
+
assert.ok(Math.abs(blackOklch.L) < 1e-3);
|
|
40
|
+
assert.ok(blackOklch.C < 1e-3);
|
|
41
|
+
|
|
42
|
+
// Test hex roundtrip for a typical color (#262626)
|
|
43
|
+
const oklch = rgbToOklch(0x26, 0x26, 0x26);
|
|
44
|
+
const back = oklchToRgb(oklch.L, oklch.C, oklch.h);
|
|
45
|
+
assert.equal(rgbToHex(back), "#262626");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("parseHexString", () => {
|
|
49
|
+
assert.deepEqual(parseHexString("#ffffff"), { r: 255, g: 255, b: 255 });
|
|
50
|
+
assert.deepEqual(parseHexString("#000"), { r: 0, g: 0, b: 0 });
|
|
51
|
+
assert.deepEqual(parseHexString("#262626"), { r: 38, g: 38, b: 38 });
|
|
52
|
+
assert.equal(parseHexString("invalid"), null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("parseOklchString", () => {
|
|
56
|
+
const parsed = parseOklchString("oklch(0.7721 0.0228 96.4)");
|
|
57
|
+
assert.ok(parsed);
|
|
58
|
+
assert.equal(parsed.L, 0.7721);
|
|
59
|
+
assert.equal(parsed.C, 0.0228);
|
|
60
|
+
assert.equal(parsed.h, 96.4);
|
|
61
|
+
|
|
62
|
+
const parsedPct = parseOklchString("oklch(75% 0.02 180deg)");
|
|
63
|
+
assert.ok(parsedPct);
|
|
64
|
+
assert.equal(parsedPct.L, 0.75);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("parseCssColor", () => {
|
|
68
|
+
const oklchColor = parseCssColor("oklch(0.7721 0.0228 96.4)");
|
|
69
|
+
assert.ok(oklchColor);
|
|
70
|
+
assert.equal(oklchColor.L, 0.7721);
|
|
71
|
+
assert.equal(oklchColor.C, 0.0228);
|
|
72
|
+
assert.equal(oklchColor.h, 96.4);
|
|
73
|
+
assert.ok(oklchColor.hex.startsWith("#"));
|
|
74
|
+
|
|
75
|
+
const hexColor = parseCssColor("#ffffff");
|
|
76
|
+
assert.equal(hexColor.L, 1);
|
|
77
|
+
assert.equal(hexColor.hex, "#ffffff");
|
|
78
|
+
|
|
79
|
+
const blackColor = parseCssColor("#000000");
|
|
80
|
+
assert.equal(blackColor.L, 0);
|
|
81
|
+
assert.equal(blackColor.hex, "#000000");
|
|
82
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stnd/lab",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"svelte": "./Lab.svelte",
|
|
6
6
|
"author": "Francis Fontaine",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"bezier-easing": "^3.0.1",
|
|
18
18
|
"svelte": "5.57.2",
|
|
19
|
-
"@stnd/ui": "0.
|
|
19
|
+
"@stnd/ui": "0.16.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"astro": ">=5.0.0"
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"wrangler": "^4.148.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test"
|
|
29
32
|
}
|
|
30
33
|
}
|