color-value-tools 1.1.4 → 1.1.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Danil Lisin Vladimirovich (macrulez)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,12 +4,58 @@
4
4
  </h1>
5
5
  <img
6
6
  src="https://s3.twcstorage.ru/c9a2cc89-780f97fd-311d-4a1a-b86f-c25665c9dc46/images/npm/color-value-tools.webp"
7
- alt="vue-virtual-scroller-kit"
7
+ alt="color-value-tools"
8
8
  style="max-width:100%;width:auto;height:300px;border-radius:12px"
9
9
  />
10
10
  </div>
11
11
 
12
- A comprehensive utility library for parsing, converting, manipulating, and analyzing color values across all major color models — hex, RGB, HSL, HSV, HWB, Lab, LCH, OKLAB, OKLCH, CMYK — plus CSS variables and named colors.
12
+ A comprehensive utility library for parsing, converting, manipulating, and analyzing color values across all major color models — hex, RGB, HSL, HSV, HWB, Lab, LCH, Oklab, Oklch, CMYK, Display P3 — plus CSS variables and named colors. Zero dependencies.
13
+
14
+ ---
15
+
16
+ ## Contents
17
+
18
+ - [Features](#features)
19
+ - [Installation](#installation)
20
+ - [Quick start](#quick-start)
21
+ - [Detection](#detection)
22
+ - [Parsing & Normalization](#parsing--normalization)
23
+ - [Conversions](#conversions)
24
+ - [Manipulation](#manipulation)
25
+ - [Interpolation & Scales](#interpolation--scales)
26
+ - [Color Harmonies](#color-harmonies)
27
+ - [Palette Generation](#palette-generation)
28
+ - [Accessibility (WCAG)](#accessibility-wcag)
29
+ - [Color Blindness Simulation](#color-blindness-simulation)
30
+ - [Formatting](#formatting)
31
+ - [Cache Management](#cache-management)
32
+ - [Generator Functions](#generator-functions)
33
+ - [CLI](#cli)
34
+ - [Cookbook](#cookbook)
35
+ - [TypeScript types](#typescript-types)
36
+ - [Architecture](#architecture)
37
+ - [Bundle size & dependencies](#bundle-size--dependencies)
38
+
39
+ ---
40
+
41
+ ## Features
42
+
43
+ - **Universal parser** — `normalizeColor()` accepts hex (3/4/6/8-digit), `rgb()` / `rgba()`, `hsl()` / `hsla()`, `hwb()`, `oklch()` / `oklcha()`, `color(display-p3 ...)`, CSS named colors (all 148), and plain `{r,g,b}` / `{h,s,l}` objects
44
+ - **Conversions** — every path between hex, RGB, HSL, HSV, HWB, Lab, LCH, Oklab, Oklch, CMYK, Display P3; all round-trip accurate
45
+ - **Manipulation** — `lighten`, `darken`, `saturate`, `desaturate`, `invertColor`, `grayscale`, `setAlpha`, `rotateHue`, `adjustHexBrightness`
46
+ - **Mixing & Interpolation** — `mixColors` and `interpolateColors` in 6 color spaces with 4 hue interpolation modes; `createColorScale` across multiple anchor colors with custom positions; `midpointColor` for a perceptual midpoint
47
+ - **Color Harmonies** — `complement`, `triadic`, `analogous`, `splitComplementary`, `tetradic`
48
+ - **Palette Generation** — `colorShades`, `monochromatic`, `tints`, `shades`, `tones` — all Oklab-accurate where applicable
49
+ - **WCAG Accessibility** — `relativeLuminance`, `contrastRatio`, `wcagLevel`, `bestTextColor`, `bestContrastColor`, `bestContrastPalette`, `isReadableOnBackground` (solid, semi-transparent, and gradient backgrounds)
50
+ - **Color Blindness Simulation** — protanopia, deuteranopia, tritanopia using Vienot 1999 matrices on linear RGB
51
+ - **Perceptual color distance** — `colorDeltaE` implementing the full CIEDE2000 formula
52
+ - **CSS Formatting** — `toHslString`, `toHwbString`, `toOklchString`, `toColorP3String`
53
+ - **Cache** — LRU-style `normalizeColorCached`; `getCacheStats`, `clearColorCache`, `enableCache` / `disableCache`
54
+ - **Generator functions** — `generateGradientColors*`, `generateTints*`, `generateShades*` for lazy iteration without pre-allocating arrays
55
+ - **CLI (`cvt`)** — inspect any color from the terminal: full info, all conversions, WCAG contrast, shades, harmonies, nearest named color
56
+ - **Zero dependencies** — no runtime deps; ships as tree-shakeable ESM + CommonJS
57
+
58
+ ---
13
59
 
14
60
  ## Installation
15
61
 
@@ -17,7 +63,11 @@ A comprehensive utility library for parsing, converting, manipulating, and analy
17
63
  npm install color-value-tools
18
64
  ```
19
65
 
20
- ## Usage
66
+ No peer dependencies required.
67
+
68
+ ---
69
+
70
+ ## Quick start
21
71
 
22
72
  ```ts
23
73
  import {
@@ -26,70 +76,100 @@ import {
26
76
  complement, triadic,
27
77
  wcagLevel, bestTextColor,
28
78
  colorDeltaE, randomColor,
29
- } from 'color-value-tools';
79
+ } from 'color-value-tools'
30
80
 
31
81
  // Parse any color format
32
- normalizeColor('#3498db');
33
- // { type: 'hex', hex: '#3498db', r: 52, g: 152, b: 219, h: 204, s: 70, l: 53, ... }
82
+ normalizeColor('#3498db')
83
+ // { type: 'hex', hex: '#3498db', r: 52, g: 152, b: 219, h: 204, s: 70, l: 53, v: 86, a: 1 }
34
84
 
35
85
  // Manipulate
36
- lighten('#3498db', 15); // '#6ab4e8'
37
- darken('#3498db', 15); // '#1a6da3'
38
- saturate('#3498db', 20); // '#1a8fe8'
86
+ lighten('#3498db', 15) // '#6ab4e8'
87
+ darken('#3498db', 15) // '#1a6da3'
88
+ saturate('#3498db', 20) // '#1a8fe8'
39
89
 
40
90
  // Harmonies
41
- complement('#3498db'); // '#db6034'
42
- triadic('#3498db'); // ['#3498db', '#db3498', '#98db34']
91
+ complement('#3498db') // '#db6034'
92
+ triadic('#3498db') // ['#3498db', '#db3498', '#98db34']
43
93
 
44
94
  // WCAG accessibility
45
- wcagLevel('#ffffff', '#3498db'); // 'AA'
46
- bestTextColor('#3498db'); // '#ffffff'
95
+ wcagLevel('#ffffff', '#3498db') // 'AA'
96
+ bestTextColor('#3498db') // '#ffffff'
47
97
 
48
98
  // Perceptual color distance (CIEDE2000)
49
- colorDeltaE('#ff0000', '#fe0000'); // ~0.9
99
+ colorDeltaE('#ff0000', '#fe0000') // ~0.9
50
100
 
51
101
  // Random color
52
- randomColor({ hRange: [200, 260], sRange: [60, 80] });
102
+ randomColor({ hRange: [200, 260], sRange: [60, 80] })
53
103
  ```
54
104
 
55
105
  CommonJS:
106
+
56
107
  ```js
57
- const { normalizeColor, mixColors } = require('color-value-tools');
108
+ const { normalizeColor, mixColors } = require('color-value-tools')
58
109
  ```
59
110
 
60
111
  ---
61
112
 
62
- ## API Reference
63
-
64
- ### Detection
113
+ ## Detection
65
114
 
66
115
  | Function | Description |
67
116
  |---|---|
68
117
  | `getColorType(value)` | Returns `'hex'` \| `'css-var'` \| `'rgb'` \| `'hsl'` \| `'named'` \| `'oklch'` \| `'color'` \| `'unknown'` |
69
- | `isHexColor(value)` | Detects 3-, 4-, 6- or 8-digit hex strings (with or without `#`) |
118
+ | `isHexColor(value)` | Detects 3-, 4-, or 6-digit hex strings (with or without `#`) |
70
119
  | `isRgbColor(value)` | Detects `rgb()` / `rgba()` strings |
71
120
  | `isHslColor(value)` | Detects `hsl()` / `hsla()` strings |
72
121
  | `isOklchColor(value)` | Detects `oklch()` / `oklcha()` strings |
73
- | `isColorFunction(value)` | Detects `color(display-p3 ...)` / `color(srgb ...)` strings |
122
+ | `isColorFunction(value)` | Detects `color(display-p3 ...)`, `color(srgb ...)`, `color(srgb-linear ...)` strings |
74
123
  | `isCssVariable(value)` | Checks for `var(--name)` pattern |
75
124
  | `extractCssVariableName(value)` | Extracts `--name` from `var(--name, fallback)` |
76
125
 
77
- ### Parsing & Normalization
126
+ ---
127
+
128
+ ## Parsing & Normalization
78
129
 
79
130
  | Function | Description |
80
131
  |---|---|
81
- | `normalizeColor(input)` | Universal parser. Accepts hex, `rgb()`, `hsl()`, `oklch()`, `color()`, named color, `{r,g,b}` / `{h,s,l}`. Returns `{ type, hex, r, g, b, a, h, s, l, v }` |
82
- | `normalizeColorCached(input)` | Same as `normalizeColor` but uses an internal LRU-style cache |
132
+ | `normalizeColor(input)` | Universal parser. Accepts hex (3/4/6/8-digit), `rgb()`, `hsl()`, `hwb()`, `oklch()`, `color()`, named color, `{r,g,b}` object, `{h,s,l}` object. Returns `{ type, hex, r, g, b, a, h, s, l, v }` |
133
+ | `normalizeColorCached(input)` | Same as `normalizeColor` but uses an internal LRU-style cache. String input only |
83
134
  | `normalizeHex(hex)` | Normalizes 3- or 6-digit hex to lowercase 6-digit with `#` |
84
- | `rgbaStringToRgba(str)` | Parses `rgb()` / `rgba()` string to `{r, g, b, a}` |
85
- | `hex8ToRgba(hex)` | Parses 8-digit hex (`#rrggbbaa`) to `{r, g, b, a}` |
135
+ | `rgbaStringToRgba(str)` | Parses `rgb()` / `rgba()` string to `{r, g, b, a}`; supports percentage channels |
136
+ | `hex8ToRgba(hex)` | Parses 8-digit hex (`#rrggbbaa`) or 4-digit hex (`#rgba`) to `{r, g, b, a}` |
86
137
  | `shortHexToRgba(hex)` | Parses 4-digit hex (`#rgba`) to `{r, g, b, a}` — e.g. `#f0f0` → `{r:255,g:0,b:255,a:0}` |
87
138
  | `parseHwbString(str)` | Parses `hwb()` string to `{H, W, B, alpha}` |
88
- | `parseOklchString(str)` | Parses `oklch(L C H / alpha)` to `{L, C, H, alpha}` |
139
+ | `parseOklchString(str)` | Parses `oklch(L C H / alpha)` to `{L, C, H, alpha}`; accepts percentage L |
89
140
  | `parseColorFn(str)` | Parses `color(display-p3 r g b / alpha)` to `{space, r, g, b, alpha}` |
90
141
  | `parseCssVar(value)` | Parses `var(--name, fallback)` to `{variableName, fallback?}` |
91
142
 
92
- ### Conversions
143
+ ### `normalizeColor` return value
144
+
145
+ ```ts
146
+ {
147
+ type: 'hex' | 'rgb' | 'hsl' | 'oklch' | 'color' | 'named' | 'css-var' | 'unknown'
148
+ hex?: string // '#rrggbb'
149
+ r?: number // 0–255
150
+ g?: number // 0–255
151
+ b?: number // 0–255
152
+ a?: number // 0–1
153
+ h?: number // hue 0–360
154
+ s?: number // HSL saturation 0–100
155
+ l?: number // HSL lightness 0–100
156
+ v?: number // HSV value 0–100
157
+ raw?: string // set for css-var type
158
+ }
159
+ ```
160
+
161
+ ```ts
162
+ // Object input
163
+ normalizeColor({ r: 52, g: 152, b: 219 }) // → full result with hex, h, s, l, v
164
+ normalizeColor({ h: 204, s: 70, l: 53 }) // → full result with hex, r, g, b, v
165
+
166
+ // CSS variable — returns raw, no color channels
167
+ normalizeColor('var(--brand-color)') // → { type: 'css-var', raw: 'var(--brand-color)' }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Conversions
93
173
 
94
174
  | Function | Description |
95
175
  |---|---|
@@ -103,6 +183,7 @@ const { normalizeColor, mixColors } = require('color-value-tools');
103
183
  | `hsvToRgb(h, s, v)` | `→ {r, g, b}` |
104
184
  | `rgbToHex({r,g,b})` | `→ #rrggbb` |
105
185
  | `rgbaToHex({r,g,b,a})` | `→ #rrggbbaa` |
186
+ | `rgbaToHex8({r,g,b,a})` | Alias for `rgbaToHex` |
106
187
  | `rgbToRgbaString({r,g,b}, a)` | `→ rgba(...)` string |
107
188
  | `rgbToHsl({r,g,b})` | `→ [h, s, l]` |
108
189
  | `rgbToHsv({r,g,b})` | `→ [h, s, v]` |
@@ -121,9 +202,10 @@ const { normalizeColor, mixColors } = require('color-value-tools');
121
202
  | `rgbToDisplayP3({r,g,b})` | `→ {r, g, b}` in Display P3 space (0–1 per channel) |
122
203
  | `displayP3ToRgb({r,g,b})` | `→ {r, g, b}` sRGB (0–255) |
123
204
  | `toDisplayP3Hex(color)` | Converts any color → Display P3 → hex |
124
- | `rgbaToHex8({r,g,b,a})` | Alias for `rgbaToHex` |
125
205
 
126
- ### Manipulation
206
+ ---
207
+
208
+ ## Manipulation
127
209
 
128
210
  | Function | Description |
129
211
  |---|---|
@@ -133,21 +215,64 @@ const { normalizeColor, mixColors } = require('color-value-tools');
133
215
  | `desaturate(color, amount)` | Decrease HSL saturation by `amount` (0–100) |
134
216
  | `setAlpha(color, alpha)` | Returns `rgba(...)` with the given alpha (0–1) |
135
217
  | `getAlpha(color)` | Returns the alpha channel value (0–1) |
136
- | `invertColor(color)` | Inverts RGB channels |
137
- | `grayscale(color)` | Converts to grayscale using ITU-R BT.709 weights |
138
- | `rotateHue(hex, degrees)` | Rotates hue by degrees (supports negative values) |
218
+ | `invertColor(color)` | Inverts all three RGB channels |
219
+ | `grayscale(color)` | Converts to grayscale using ITU-R BT.709 perceptual weights |
220
+ | `rotateHue(hex, degrees)` | Rotates hue by degrees; supports negative values |
139
221
  | `adjustHexBrightness(hex, offsetPercent)` | Lightens (positive) or darkens (negative) by percentage |
140
- | `mixColors(c1, c2, t, opts?)` | Interpolates between two colors. `t` = 0–1. `mode`: `'rgb'`\|`'hsl'`\|`'lab'`\|`'lch'`\|`'oklab'`\|`'oklch'`. `hueInterpolation`: `'shorter'`\|`'longer'`\|`'increasing'`\|`'decreasing'` |
222
+ | `mixColors(c1, c2, t, opts?)` | Interpolates between two colors. `t` = 0–1. `mode`: `'rgb'` \| `'hsl'` \| `'lab'` \| `'lch'` \| `'oklab'` \| `'oklch'`. `hueInterpolation`: `'shorter'` \| `'longer'` \| `'increasing'` \| `'decreasing'` |
223
+
224
+ ### `mixColors` example
225
+
226
+ ```ts
227
+ import { mixColors } from 'color-value-tools'
228
+
229
+ // RGB mix at midpoint
230
+ mixColors('#e74c3c', '#3498db', 0.5)
231
+ // → '#8dc6bc'
141
232
 
142
- ### Interpolation & Scales
233
+ // Perceptually even mix in Oklab
234
+ mixColors('#e74c3c', '#3498db', 0.5, { mode: 'oklab', format: 'hex' })
235
+
236
+ // HSL with "longer" hue path
237
+ mixColors('#e74c3c', '#3498db', 0.5, { mode: 'hsl', hueInterpolation: 'longer' })
238
+
239
+ // Get rgba string output
240
+ mixColors('#ff0000', '#0000ff', 0.25, { mode: 'lab', format: 'rgba' })
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Interpolation & Scales
143
246
 
144
247
  | Function | Description |
145
248
  |---|---|
146
- | `interpolateColors(c1, c2, steps, opts?)` | Returns array of `steps` colors from `c1` to `c2`. Same `space`/`format`/`hueInterpolation` options as `mixColors` |
249
+ | `interpolateColors(c1, c2, steps, opts?)` | Returns array of `steps` colors from `c1` to `c2`. Same `space` / `format` / `hueInterpolation` options as `mixColors` |
147
250
  | `createColorScale(anchors, steps, opts?)` | Generates a `steps`-color scale across multiple anchor colors with optional positions (0–1) |
148
251
  | `midpointColor(c1, c2, opts?)` | Perceptual midpoint between two colors (default space: `'oklab'`) |
149
252
 
150
- ### Color Harmonies
253
+ ### `createColorScale` — multi-stop gradient
254
+
255
+ ```ts
256
+ import { createColorScale } from 'color-value-tools'
257
+
258
+ // Evenly spaced anchors
259
+ createColorScale(['#ff0000', '#ffff00', '#00ff00'], 9)
260
+
261
+ // With explicit positions
262
+ createColorScale(
263
+ [
264
+ { color: '#1a1a2e', position: 0 },
265
+ { color: '#e94560', position: 0.4 },
266
+ { color: '#f5a623', position: 1 },
267
+ ],
268
+ 12,
269
+ { space: 'oklch' }
270
+ )
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Color Harmonies
151
276
 
152
277
  | Function | Description |
153
278
  |---|---|
@@ -157,7 +282,17 @@ const { normalizeColor, mixColors } = require('color-value-tools');
157
282
  | `splitComplementary(color)` | Base + two colors at 150° and 210° |
158
283
  | `tetradic(color)` | 4 colors evenly spaced 90° apart |
159
284
 
160
- ### Palette Generation
285
+ ```ts
286
+ import { triadic, analogous, tetradic } from 'color-value-tools'
287
+
288
+ triadic('#6c3483') // ['#6c3483', '#34836c', '#83346c']
289
+ analogous('#6c3483', 45) // ['#833469', '#6c3483', '#3469…']
290
+ tetradic('#6c3483') // 4 colors
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Palette Generation
161
296
 
162
297
  | Function | Description |
163
298
  |---|---|
@@ -167,7 +302,9 @@ const { normalizeColor, mixColors } = require('color-value-tools');
167
302
  | `shades(color, steps?)` | Mix toward black in Oklab (default 5 steps) |
168
303
  | `tones(color, steps?, gray?)` | Mix toward gray in Oklab (default 5 steps, gray `#808080`) |
169
304
 
170
- ### Accessibility (WCAG)
305
+ ---
306
+
307
+ ## Accessibility (WCAG)
171
308
 
172
309
  | Function | Description |
173
310
  |---|---|
@@ -176,14 +313,56 @@ const { normalizeColor, mixColors } = require('color-value-tools');
176
313
  | `wcagLevel(fg, bg)` | Returns `'AAA'` \| `'AA'` \| `'AA-large'` \| `'fail'` |
177
314
  | `bestTextColor(bg)` | Returns `'#000000'` or `'#ffffff'` for best contrast on `bg` |
178
315
  | `bestContrastColor(bg, candidates)` | Picks the most readable color from an array of candidates |
179
- | `bestContrastPalette(bg, palettes, opts?)` | Picks the palette with best overall contrast. Returns `{paletteIndex, palette, minContrastRatio, avgContrastRatio}` |
180
- | `isReadableOnBackground(text, bg, opts?)` | Checks readability on solid, semi-transparent, or gradient backgrounds. Returns `{readable, minContrastRatio, wcagLevel}` |
316
+ | `bestContrastPalette(bg, palettes, opts?)` | Picks the palette with best overall contrast. Returns `{ paletteIndex, palette, minContrastRatio, avgContrastRatio }` |
317
+ | `isReadableOnBackground(text, bg, opts?)` | Checks readability on solid, semi-transparent, or gradient backgrounds. Returns `{ readable, minContrastRatio, wcagLevel }` |
181
318
  | `isDark(color, threshold?)` | `true` if luminance is below threshold (default 0.5) |
182
319
  | `isLight(color, threshold?)` | Inverse of `isDark` |
183
320
 
184
- ### Color Blindness Simulation
321
+ ### `isReadableOnBackground` complex backgrounds
322
+
323
+ `background` can be a plain color string, a semi-transparent spec, or a gradient with multiple stops:
185
324
 
186
- Simulates perception using Vienot 1999 matrices applied to linear RGB.
325
+ ```ts
326
+ import { isReadableOnBackground } from 'color-value-tools'
327
+
328
+ // Solid background
329
+ isReadableOnBackground('#ffffff', '#3498db')
330
+ // → { readable: true, minContrastRatio: 3.05, wcagLevel: 'AA-large' }
331
+
332
+ // Semi-transparent overlay composited over white
333
+ isReadableOnBackground('#ffffff', {
334
+ type: 'semi-transparent',
335
+ color: 'rgba(52, 152, 219, 0.5)',
336
+ underlay: '#f0f0f0',
337
+ })
338
+
339
+ // Gradient — worst stop is used for the result
340
+ isReadableOnBackground('#ffffff', {
341
+ type: 'gradient',
342
+ stops: ['#1a1a2e', '#e94560', '#f5a623'],
343
+ })
344
+
345
+ // AAA + large text
346
+ isReadableOnBackground('#000000', '#f0f0f0', { level: 'AAA', largeText: true })
347
+ ```
348
+
349
+ ### `bestContrastPalette` — pick accessible palette
350
+
351
+ ```ts
352
+ import { bestContrastPalette } from 'color-value-tools'
353
+
354
+ const result = bestContrastPalette('#1a1a2e', [
355
+ ['#ffffff', '#f0f0f0', '#cccccc'],
356
+ ['#ffff00', '#ffd700', '#ff8c00'],
357
+ ])
358
+ // → { paletteIndex: 0, palette: [...], minContrastRatio: 12.1, avgContrastRatio: 14.3 }
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Color Blindness Simulation
364
+
365
+ Simulates perception using Vienot 1999 matrices applied to linearized RGB.
187
366
 
188
367
  | Function | Description |
189
368
  |---|---|
@@ -192,7 +371,16 @@ Simulates perception using Vienot 1999 matrices applied to linear RGB.
192
371
  | `simulateTritanopia(color)` | No S-cones (blue-blind) |
193
372
  | `simulateColorBlindness(color, type)` | Generic — `type`: `'protanopia'` \| `'deuteranopia'` \| `'tritanopia'` |
194
373
 
195
- ### Formatting
374
+ ```ts
375
+ import { simulateColorBlindness } from 'color-value-tools'
376
+
377
+ simulateColorBlindness('#e74c3c', 'deuteranopia') // '#9a7b00'
378
+ simulateColorBlindness('#3498db', 'protanopia') // '#5282db'
379
+ ```
380
+
381
+ ---
382
+
383
+ ## Formatting
196
384
 
197
385
  | Function | Description |
198
386
  |---|---|
@@ -201,48 +389,81 @@ Simulates perception using Vienot 1999 matrices applied to linear RGB.
201
389
  | `toOklchString(color, alpha?)` | Converts any color to `oklch(L C H)` CSS string |
202
390
  | `toColorP3String(color, alpha?)` | Converts any color to `color(display-p3 r g b)` CSS string |
203
391
 
204
- ### Cache Management
392
+ ```ts
393
+ import { toOklchString, toColorP3String, toHwbString } from 'color-value-tools'
205
394
 
206
- | Function | Description |
207
- |---|---|
208
- | `normalizeColorCached(input)` | Cached version of `normalizeColor` for repeated calls |
209
- | `clearColorCache()` | Clears the normalization cache |
210
- | `getCacheStats()` | Returns `{size, hits}` |
211
- | `enableCache()` / `disableCache()` | Toggle caching on/off |
395
+ toOklchString('#3498db') // 'oklch(0.6 0.1175 234.77)'
396
+ toColorP3String('#3498db') // 'color(display-p3 0.2493 0.5875 0.8479)'
397
+ toHwbString(204, 20, 14) // 'hwb(204 20% 14%)'
398
+ toHwbString(204, 20, 14, 0.8) // 'hwb(204 20% 14% / 0.8)'
399
+ ```
400
+
401
+ ---
212
402
 
213
- ### Generator Functions
403
+ ## Cache Management
214
404
 
215
- Useful for large palettes and frame-by-frame animations without allocating full arrays.
405
+ Useful in rendering loops, color pickers, or any context with repeated `normalizeColor` calls on the same values.
216
406
 
217
407
  | Function | Description |
218
408
  |---|---|
219
- | `generateGradientColors*(start, end, steps, opts?)` | Yields `steps` colors from `start` to `end` |
220
- | `generateTints*(color, steps, opts?)` | Yields tints toward white |
221
- | `generateShades*(color, steps, opts?)` | Yields shades toward black |
409
+ | `normalizeColorCached(input)` | Cached version of `normalizeColor` for repeated string calls |
410
+ | `clearColorCache()` | Clears the normalization cache and resets hit counter |
411
+ | `getCacheStats()` | Returns `{ size: number, hits: number }` |
412
+ | `enableCache()` | Re-enables the cache (on by default) |
413
+ | `disableCache()` | Disables caching (useful in tests) |
414
+
415
+ ```ts
416
+ import { normalizeColorCached, getCacheStats, clearColorCache } from 'color-value-tools'
222
417
 
223
- ### Utilities
418
+ // First call — parsed and cached
419
+ normalizeColorCached('#3498db')
420
+
421
+ // Second call — instant cache hit
422
+ normalizeColorCached('#3498db')
423
+
424
+ getCacheStats() // { size: 1, hits: 1 }
425
+ clearColorCache() // { size: 0, hits: 0 } reset
426
+ ```
427
+
428
+ ---
429
+
430
+ ## Generator Functions
431
+
432
+ Useful for large palettes and frame-by-frame animations without allocating full arrays upfront.
224
433
 
225
434
  | Function | Description |
226
435
  |---|---|
227
- | `colorDeltaE(c1, c2)` | Perceptual color distance using CIEDE2000 |
228
- | `randomColor(options?)` | Generates a random color. Options: `hRange`, `sRange`, `lRange` (each `[min, max]`) |
229
- | `toNearestNamedColor(color)` | Returns the closest CSS named color name (all 148 standard colors) |
436
+ | `generateGradientColors*(start, end, steps, opts?)` | Yields `steps` colors from `start` to `end`. Same `mode` / `format` options as `mixColors` |
437
+ | `generateTints*(color, steps, opts?)` | Yields tints toward white in Oklab |
438
+ | `generateShades*(color, steps, opts?)` | Yields shades toward black in Oklab |
439
+
440
+ ```ts
441
+ import { generateGradientColors, generateTints } from 'color-value-tools'
442
+
443
+ // Process one color at a time — no intermediate array
444
+ for (const color of generateGradientColors('#ff0000', '#0000ff', 1000, { mode: 'oklch' })) {
445
+ renderPixel(color)
446
+ }
447
+
448
+ // Collect lazily
449
+ const first5 = [...generateTints('#e74c3c', 100)].slice(0, 5)
450
+ ```
230
451
 
231
452
  ---
232
453
 
233
454
  ## CLI
234
455
 
235
- After installing globally or via `npx`, you can inspect colors directly from the terminal:
456
+ The `cvt` CLI is included in the package. Install globally or use via `npx`:
236
457
 
237
458
  ```bash
238
459
  npm install -g color-value-tools
239
460
  ```
240
461
 
241
462
  ```bash
242
- # Full info (default)
463
+ # Full info (default command)
243
464
  cvt "#3498db"
244
465
 
245
- # All format conversions
466
+ # All format conversions — hex, rgb, hsl, hsv, hwb, lab, lch, oklab, oklch, cmyk
246
467
  cvt "#3498db" convert
247
468
 
248
469
  # Contrast ratio and WCAG level
@@ -251,13 +472,27 @@ cvt "#3498db" contrast "#ffffff"
251
472
  # Generate 7 shades
252
473
  cvt "#3498db" shades 7
253
474
 
254
- # All harmonies
475
+ # All harmonies — complement, triadic, analogous, split-comp, tetradic
255
476
  cvt "#3498db" harmonies
256
477
 
257
478
  # Nearest CSS named color
258
479
  cvt "cornflowerblue" nearest
259
480
  ```
260
481
 
482
+ Accepts any valid color format: hex, `rgb()`, `hsl()`, `oklch()`, named colors.
483
+
484
+ ```
485
+ cvt "#3498db"
486
+ ────────────────────────────────────────
487
+ Type: hex
488
+ Hex: #3498db
489
+ RGB: rgb(52, 152, 219)
490
+ HSL: hsl(204, 70%, 53%)
491
+ HSV: hsv(204, 76%, 86%)
492
+ Nearest named: cornflowerblue
493
+ Best text on it: #ffffff
494
+ ```
495
+
261
496
  ---
262
497
 
263
498
  ## Cookbook
@@ -265,78 +500,253 @@ cvt "cornflowerblue" nearest
265
500
  ### Generate an accessible button palette
266
501
 
267
502
  ```ts
268
- import { colorShades, bestTextColor, wcagLevel } from 'color-value-tools';
503
+ import { colorShades, bestTextColor, wcagLevel } from 'color-value-tools'
269
504
 
270
505
  function buttonPalette(base: string) {
271
- const shades = colorShades(base, 9);
272
- return shades.map(shade => ({
273
- bg: shade,
506
+ return colorShades(base, 9).map(shade => ({
507
+ bg: shade,
274
508
  text: bestTextColor(shade),
275
509
  wcag: wcagLevel(bestTextColor(shade), shade),
276
- }));
510
+ }))
277
511
  }
278
512
 
279
- buttonPalette('#3498db');
513
+ buttonPalette('#3498db')
280
514
  // [{ bg: '#ffffff', text: '#000000', wcag: 'AAA' }, ...]
281
515
  ```
282
516
 
283
- ### Theme-aware dark/light color
517
+ ### Theme-aware color adaptation
284
518
 
285
519
  ```ts
286
- import { isDark, lighten, darken } from 'color-value-tools';
520
+ import { isDark, lighten, darken } from 'color-value-tools'
287
521
 
288
522
  function adaptToTheme(color: string, isDarkTheme: boolean): string {
289
- return isDarkTheme ? lighten(color, 20) : darken(color, 10);
523
+ return isDarkTheme ? lighten(color, 20) : darken(color, 10)
290
524
  }
291
525
  ```
292
526
 
293
- ### Mix two brand colors at a midpoint
527
+ ### Mix two brand colors at a perceptual midpoint
294
528
 
295
529
  ```ts
296
- import { mixColors } from 'color-value-tools';
530
+ import { midpointColor } from 'color-value-tools'
297
531
 
298
- const mid = mixColors('#e74c3c', '#3498db', 0.5, { mode: 'hsl', format: 'hex' });
299
- // Perceptually even blend between red and blue
532
+ const mid = midpointColor('#e74c3c', '#3498db') // Oklab midpoint
533
+ const midLch = midpointColor('#e74c3c', '#3498db', { space: 'oklch' })
300
534
  ```
301
535
 
302
- ### Build a triadic color scheme and check contrast
536
+ ### Build a triadic scheme and check contrast
303
537
 
304
538
  ```ts
305
- import { triadic, contrastRatio } from 'color-value-tools';
539
+ import { triadic, contrastRatio } from 'color-value-tools'
306
540
 
307
- const [base, second, third] = triadic('#6c3483');
308
- console.log(contrastRatio(base, '#ffffff')); // e.g. 8.4
309
- console.log(contrastRatio(second, '#ffffff'));
541
+ const [base, second, third] = triadic('#6c3483')
542
+ console.log(contrastRatio(base, '#ffffff')) // e.g. 8.4
543
+ console.log(contrastRatio(second, '#ffffff'))
310
544
  ```
311
545
 
312
- ### Convert a CSS color string to all formats at once
546
+ ### Convert any color string to all formats at once
313
547
 
314
548
  ```ts
315
- import { normalizeColor, rgbToOklch, rgbToCmyk } from 'color-value-tools';
549
+ import { normalizeColor, rgbToOklch, rgbToCmyk, toColorP3String } from 'color-value-tools'
316
550
 
317
- const n = normalizeColor('hsl(204, 70%, 53%)');
318
- const oklch = rgbToOklch({ r: n.r!, g: n.g!, b: n.b! });
319
- const cmyk = rgbToCmyk({ r: n.r!, g: n.g!, b: n.b! });
320
- console.log(n.hex, oklch, cmyk);
551
+ const n = normalizeColor('hsl(204, 70%, 53%)')
552
+ const oklch = rgbToOklch({ r: n.r!, g: n.g!, b: n.b! })
553
+ const cmyk = rgbToCmyk({ r: n.r!, g: n.g!, b: n.b! })
554
+ const p3 = toColorP3String(n.hex!)
555
+ console.log(n.hex, oklch, cmyk, p3)
321
556
  ```
322
557
 
323
- ### Find the perceptually nearest named CSS color
558
+ ### Find the nearest CSS named color
324
559
 
325
560
  ```ts
326
- import { toNearestNamedColor } from 'color-value-tools';
561
+ import { toNearestNamedColor } from 'color-value-tools'
327
562
 
328
- toNearestNamedColor('#1a8ccc'); // 'steelblue'
563
+ toNearestNamedColor('#1a8ccc') // 'steelblue'
564
+ toNearestNamedColor('#e74c3c') // 'tomato'
329
565
  ```
330
566
 
331
567
  ### Random palette within a hue range
332
568
 
333
569
  ```ts
334
- import { randomColor, colorShades } from 'color-value-tools';
570
+ import { randomColor, colorShades } from 'color-value-tools'
571
+
572
+ const accent = randomColor({ hRange: [200, 260], sRange: [60, 80], lRange: [40, 60] })
573
+ const palette = colorShades(accent, 5)
574
+ ```
575
+
576
+ ### Simulate color blindness for a palette
577
+
578
+ ```ts
579
+ import { triadic, simulateColorBlindness } from 'color-value-tools'
580
+
581
+ const palette = triadic('#e74c3c')
582
+ const deuteranopia = palette.map(c => simulateColorBlindness(c, 'deuteranopia'))
583
+ // Compare palette vs deuteranopia to verify distinguishability
584
+ ```
585
+
586
+ ### Lazy gradient — process 10 000 colors one at a time
587
+
588
+ ```ts
589
+ import { generateGradientColors } from 'color-value-tools'
590
+
591
+ for (const color of generateGradientColors('#1a1a2e', '#f5a623', 10_000, { mode: 'oklch' })) {
592
+ ctx.fillStyle = color
593
+ ctx.fillRect(x++, 0, 1, height)
594
+ }
595
+ ```
596
+
597
+ ---
598
+
599
+ ## TypeScript types
600
+
601
+ All public types are exported from the package root:
602
+
603
+ ```ts
604
+ import type {
605
+ ColorType, // 'hex' | 'css-var' | 'rgb' | 'hsl' | 'named' | 'oklch' | 'color' | 'unknown'
606
+ WcagLevel, // 'AAA' | 'AA' | 'AA-large' | 'fail'
607
+ ColorBlindnessType, // 'protanopia' | 'deuteranopia' | 'tritanopia'
608
+ BackgroundSpec, // string | { type: 'semi-transparent'; ... } | { type: 'gradient'; ... }
609
+ PaletteScore, // { palette, minContrastRatio, avgContrastRatio }
610
+ } from 'color-value-tools'
611
+ ```
612
+
613
+ ### `BackgroundSpec` — discriminated union
614
+
615
+ ```ts
616
+ import type { BackgroundSpec } from 'color-value-tools'
617
+
618
+ const solid: BackgroundSpec = '#3498db'
335
619
 
336
- const accent = randomColor({ hRange: [200, 260], sRange: [60, 80], lRange: [40, 60] });
337
- const palette = colorShades(accent, 5);
620
+ const semiTransparent: BackgroundSpec = {
621
+ type: 'semi-transparent',
622
+ color: 'rgba(0,0,0,0.4)',
623
+ underlay: '#ffffff', // optional, defaults to white
624
+ }
625
+
626
+ const gradient: BackgroundSpec = {
627
+ type: 'gradient',
628
+ stops: ['#1a1a2e', '#e94560', '#f5a623'],
629
+ }
338
630
  ```
339
631
 
632
+ ### `PaletteScore` — from `bestContrastPalette`
633
+
634
+ ```ts
635
+ import type { PaletteScore } from 'color-value-tools'
636
+
637
+ const result: PaletteScore & { paletteIndex: number } = bestContrastPalette(bg, palettes)
638
+ result.paletteIndex // number
639
+ result.palette // string[]
640
+ result.minContrastRatio // number
641
+ result.avgContrastRatio // number
642
+ ```
643
+
644
+ ### Inference from `normalizeColor`
645
+
646
+ ```ts
647
+ import { normalizeColor } from 'color-value-tools'
648
+
649
+ const n = normalizeColor('#3498db')
650
+ // TypeScript knows: n.hex, n.r, n.g, n.b, n.h, n.s, n.l, n.v, n.a, n.type
651
+
652
+ if (n.type !== 'unknown' && n.type !== 'css-var') {
653
+ const r: number = n.r! // available for all resolved color types
654
+ }
655
+ ```
656
+
657
+ ---
658
+
659
+ ## Architecture
660
+
661
+ ```
662
+ color-value-tools
663
+
664
+ ├── Detection
665
+ │ getColorType, isHexColor, isRgbColor, isHslColor,
666
+ │ isOklchColor, isColorFunction, isCssVariable
667
+
668
+ ├── Parsing
669
+ │ normalizeColor → universal entry point; handles all formats + objects
670
+ │ rgbaStringToRgba → rgb()/rgba() parser
671
+ │ hex8ToRgba → 8-digit and 4-digit hex with alpha
672
+ │ parseOklchString → oklch(L C H / alpha) with % L support
673
+ │ parseColorFn → color(display-p3 ...) / color(srgb ...)
674
+ │ parseHwbString → hwb() string
675
+ │ parseCssVar → var(--name, fallback)
676
+
677
+ ├── Color Math — every space via RGB as the hub
678
+ │ sRGB ↔ Linear (srgbChanToLinear / linearChanToSrgb)
679
+ │ XYZ D65 intermediate for Lab/LCH
680
+ │ Oklab/Oklch — Björn Ottosson matrices
681
+ │ Display P3 — ICC sRGB→P3 and P3→sRGB matrices
682
+
683
+ ├── Manipulation
684
+ │ lighten / darken / saturate / desaturate — via HSL
685
+ │ invertColor — RGB invert
686
+ │ grayscale — BT.709 perceptual weights
687
+ │ rotateHue — via HSL hue shift
688
+ │ adjustHexBrightness — linear channel blend toward 0/255
689
+ │ setAlpha / getAlpha — rgba string manipulation
690
+
691
+ ├── mixColors (core of interpolation system)
692
+ │ 6 color space modes: rgb | hsl | lab | lch | oklab | oklch
693
+ │ 4 hue interpolation modes: shorter | longer | increasing | decreasing
694
+ │ 4 output formats: hex | rgb | rgba | hsl
695
+ │ Used internally by: interpolateColors, createColorScale,
696
+ │ midpointColor, tints, shades, tones,
697
+ │ generateGradientColors*, generateTints*, generateShades*
698
+
699
+ ├── Color Harmonies
700
+ │ All implemented as hue rotations on normalized hex
701
+
702
+ ├── Palette Generation
703
+ │ colorShades — HSL lightness sweep
704
+ │ monochromatic — HSL saturation sweep
705
+ │ tints / shades / tones — Oklab interpolation via mixColors
706
+
707
+ ├── Accessibility
708
+ │ relativeLuminance — WCAG linearization formula
709
+ │ contrastRatio — (L1+0.05)/(L2+0.05)
710
+ │ wcagLevel — ratio thresholds 3 / 4.5 / 7
711
+ │ isReadableOnBackground — handles 3 background types;
712
+ │ semi-transparent: alpha-composites onto underlay before ratio check
713
+ │ gradient: evaluates each stop, uses minimum ratio
714
+ │ bestContrastPalette — score = avg×0.4 + min×0.6
715
+
716
+ ├── Color Blindness
717
+ │ Vienot 1999 matrices on linear RGB
718
+ │ 3 types: protanopia / deuteranopia / tritanopia
719
+
720
+ ├── Cache (Section 5.1)
721
+ │ Simple Map<string, NormalizeResult>
722
+ │ normalizeColorCached — Map lookup before calling normalizeColor
723
+ │ getCacheStats / clearColorCache / enableCache / disableCache
724
+
725
+ ├── Generator functions (Section 5.2)
726
+ │ ES2015 generator syntax (*) — yield one color at a time
727
+ │ generateGradientColors* → mixColors inside the generator
728
+ │ generateTints* / generateShades* → Oklab mix toward white/black
729
+
730
+ └── CLI (dist/cli/bin/cli.js → cvt)
731
+ Commands: info | convert | contrast | shades | harmonies | nearest
732
+ Accepts any color format normalizeColor understands
733
+ ```
734
+
735
+ ---
736
+
737
+ ## Bundle size & dependencies
738
+
739
+ | | |
740
+ |---|---|
741
+ | **Runtime dependencies** | None |
742
+ | **Peer dependencies** | None |
743
+ | **Dev dependencies** | TypeScript, Vitest |
744
+ | **ESM entry** | `dist/esm/index.js` (tree-shakeable) |
745
+ | **CJS entry** | `dist/cjs/index.js` |
746
+ | **CLI entry** | `dist/cli/bin/cli.js` → `cvt` |
747
+
748
+ The package ships both ESM (`type: module`) and CommonJS modules. Every function is a named export — unused functions are eliminated by bundlers that support tree-shaking.
749
+
340
750
  ---
341
751
 
342
752
  ## License
@@ -363,4 +773,4 @@ Open source takes time and effort. If my work saves you time or brings value, co
363
773
  <img src="https://img.shields.io/badge/Donate-CryptoCloud-8A2BE2?style=for-the-badge&logo=cryptocurrency&logoColor=white" alt="Donate via CryptoCloud">
364
774
  </a>
365
775
 
366
- Thank you for being part of this journey. ❤️
776
+ Thank you for being part of this journey. ❤️
package/dist/cjs/index.js CHANGED
@@ -236,11 +236,11 @@ function displayP3ToRgb(p3) {
236
236
  const sr = 1.2247 * rl + (-0.2247) * gl + 0.0000 * bl;
237
237
  const sg = (-0.0421) * rl + 1.0432 * gl + 0.0000 * bl;
238
238
  const sb = (-0.0197) * rl + (-0.0786) * gl + 1.0983 * bl;
239
- // Apply sRGB gamma
239
+ // Apply sRGB gamma and scale to 0-255
240
240
  return {
241
- r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr)))),
242
- g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg)))),
243
- b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb)))),
241
+ r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr) * 255))),
242
+ g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg) * 255))),
243
+ b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb) * 255))),
244
244
  };
245
245
  }
246
246
  // Helpers for Display P3 (operate on 0-1 values)
@@ -254,8 +254,11 @@ function linearChanToSrgb(v) {
254
254
  function toDisplayP3Hex(color) {
255
255
  const n = normalizeColorRaw(color);
256
256
  const p3 = rgbToDisplayP3({ r: n.r, g: n.g, b: n.b });
257
- const back = displayP3ToRgb(p3);
258
- return rgbToHex(back);
257
+ return rgbToHex({
258
+ r: Math.round(Math.max(0, Math.min(255, p3.r * 255))),
259
+ g: Math.round(Math.max(0, Math.min(255, p3.g * 255))),
260
+ b: Math.round(Math.max(0, Math.min(255, p3.b * 255))),
261
+ });
259
262
  }
260
263
  // Internal raw normalizer (no cache, no circular dep issues)
261
264
  function normalizeColorRaw(color) {
@@ -660,13 +663,7 @@ function normalizeColor(input) {
660
663
  const str = input.trim();
661
664
  if (isCssVariable(str))
662
665
  return { type: 'css-var', raw: str };
663
- if (isHexColor(str)) {
664
- const hex = normalizeHex(str);
665
- const [r, g, b] = hexToRgb(hex);
666
- const [h, s, l] = hexToHsl(hex);
667
- const [hh, ss, vv] = rgbToHsv({ r, g, b });
668
- return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
669
- }
666
+ // 8-digit (#rrggbbaa) or 4-digit (#rgba) — check before isHexColor
670
667
  const hex8 = str.match(/^#([0-9a-f]{8}|[0-9a-f]{4})$/i);
671
668
  if (hex8) {
672
669
  const rgba = hex8ToRgba(str);
@@ -678,6 +675,13 @@ function normalizeColor(input) {
678
675
  return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
679
676
  }
680
677
  }
678
+ if (isHexColor(str)) {
679
+ const hex = normalizeHex(str);
680
+ const [r, g, b] = hexToRgb(hex);
681
+ const [h, s, l] = hexToHsl(hex);
682
+ const [hh, ss, vv] = rgbToHsv({ r, g, b });
683
+ return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
684
+ }
681
685
  if (isRgbColor(str)) {
682
686
  const rgba = rgbaStringToRgba(str);
683
687
  if (rgba) {
@@ -704,17 +708,6 @@ function normalizeColor(input) {
704
708
  }
705
709
  if (str === 'transparent')
706
710
  return { type: 'named', hex: '#000000', r: 0, g: 0, b: 0, a: 0 };
707
- // 4-digit #RGBA hex
708
- if (/^#[0-9a-f]{4}$/i.test(str)) {
709
- const rgba = shortHexToRgba(str);
710
- if (rgba) {
711
- const { r, g, b, a } = rgba;
712
- const hex = rgbToHex({ r, g, b });
713
- const [h, s, l] = rgbToHsl({ r, g, b });
714
- const [, , vv] = rgbToHsv({ r, g, b });
715
- return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
716
- }
717
- }
718
711
  // oklch / oklcha
719
712
  if (isOklchColor(str)) {
720
713
  const parsed = parseOklchString(str);
@@ -236,11 +236,11 @@ function displayP3ToRgb(p3) {
236
236
  const sr = 1.2247 * rl + (-0.2247) * gl + 0.0000 * bl;
237
237
  const sg = (-0.0421) * rl + 1.0432 * gl + 0.0000 * bl;
238
238
  const sb = (-0.0197) * rl + (-0.0786) * gl + 1.0983 * bl;
239
- // Apply sRGB gamma
239
+ // Apply sRGB gamma and scale to 0-255
240
240
  return {
241
- r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr)))),
242
- g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg)))),
243
- b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb)))),
241
+ r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr) * 255))),
242
+ g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg) * 255))),
243
+ b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb) * 255))),
244
244
  };
245
245
  }
246
246
  // Helpers for Display P3 (operate on 0-1 values)
@@ -254,8 +254,11 @@ function linearChanToSrgb(v) {
254
254
  function toDisplayP3Hex(color) {
255
255
  const n = normalizeColorRaw(color);
256
256
  const p3 = rgbToDisplayP3({ r: n.r, g: n.g, b: n.b });
257
- const back = displayP3ToRgb(p3);
258
- return rgbToHex(back);
257
+ return rgbToHex({
258
+ r: Math.round(Math.max(0, Math.min(255, p3.r * 255))),
259
+ g: Math.round(Math.max(0, Math.min(255, p3.g * 255))),
260
+ b: Math.round(Math.max(0, Math.min(255, p3.b * 255))),
261
+ });
259
262
  }
260
263
  // Internal raw normalizer (no cache, no circular dep issues)
261
264
  function normalizeColorRaw(color) {
@@ -660,13 +663,7 @@ function normalizeColor(input) {
660
663
  const str = input.trim();
661
664
  if (isCssVariable(str))
662
665
  return { type: 'css-var', raw: str };
663
- if (isHexColor(str)) {
664
- const hex = normalizeHex(str);
665
- const [r, g, b] = hexToRgb(hex);
666
- const [h, s, l] = hexToHsl(hex);
667
- const [hh, ss, vv] = rgbToHsv({ r, g, b });
668
- return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
669
- }
666
+ // 8-digit (#rrggbbaa) or 4-digit (#rgba) — check before isHexColor
670
667
  const hex8 = str.match(/^#([0-9a-f]{8}|[0-9a-f]{4})$/i);
671
668
  if (hex8) {
672
669
  const rgba = hex8ToRgba(str);
@@ -678,6 +675,13 @@ function normalizeColor(input) {
678
675
  return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
679
676
  }
680
677
  }
678
+ if (isHexColor(str)) {
679
+ const hex = normalizeHex(str);
680
+ const [r, g, b] = hexToRgb(hex);
681
+ const [h, s, l] = hexToHsl(hex);
682
+ const [hh, ss, vv] = rgbToHsv({ r, g, b });
683
+ return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
684
+ }
681
685
  if (isRgbColor(str)) {
682
686
  const rgba = rgbaStringToRgba(str);
683
687
  if (rgba) {
@@ -704,17 +708,6 @@ function normalizeColor(input) {
704
708
  }
705
709
  if (str === 'transparent')
706
710
  return { type: 'named', hex: '#000000', r: 0, g: 0, b: 0, a: 0 };
707
- // 4-digit #RGBA hex
708
- if (/^#[0-9a-f]{4}$/i.test(str)) {
709
- const rgba = shortHexToRgba(str);
710
- if (rgba) {
711
- const { r, g, b, a } = rgba;
712
- const hex = rgbToHex({ r, g, b });
713
- const [h, s, l] = rgbToHsl({ r, g, b });
714
- const [, , vv] = rgbToHsv({ r, g, b });
715
- return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
716
- }
717
- }
718
711
  // oklch / oklcha
719
712
  if (isOklchColor(str)) {
720
713
  const parsed = parseOklchString(str);
package/dist/esm/index.js CHANGED
@@ -136,11 +136,11 @@ export function displayP3ToRgb(p3) {
136
136
  const sr = 1.2247 * rl + (-0.2247) * gl + 0.0000 * bl;
137
137
  const sg = (-0.0421) * rl + 1.0432 * gl + 0.0000 * bl;
138
138
  const sb = (-0.0197) * rl + (-0.0786) * gl + 1.0983 * bl;
139
- // Apply sRGB gamma
139
+ // Apply sRGB gamma and scale to 0-255
140
140
  return {
141
- r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr)))),
142
- g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg)))),
143
- b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb)))),
141
+ r: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sr) * 255))),
142
+ g: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sg) * 255))),
143
+ b: Math.round(Math.max(0, Math.min(255, linearChanToSrgb(sb) * 255))),
144
144
  };
145
145
  }
146
146
  // Helpers for Display P3 (operate on 0-1 values)
@@ -154,8 +154,11 @@ function linearChanToSrgb(v) {
154
154
  export function toDisplayP3Hex(color) {
155
155
  const n = normalizeColorRaw(color);
156
156
  const p3 = rgbToDisplayP3({ r: n.r, g: n.g, b: n.b });
157
- const back = displayP3ToRgb(p3);
158
- return rgbToHex(back);
157
+ return rgbToHex({
158
+ r: Math.round(Math.max(0, Math.min(255, p3.r * 255))),
159
+ g: Math.round(Math.max(0, Math.min(255, p3.g * 255))),
160
+ b: Math.round(Math.max(0, Math.min(255, p3.b * 255))),
161
+ });
159
162
  }
160
163
  // Internal raw normalizer (no cache, no circular dep issues)
161
164
  function normalizeColorRaw(color) {
@@ -560,13 +563,7 @@ export function normalizeColor(input) {
560
563
  const str = input.trim();
561
564
  if (isCssVariable(str))
562
565
  return { type: 'css-var', raw: str };
563
- if (isHexColor(str)) {
564
- const hex = normalizeHex(str);
565
- const [r, g, b] = hexToRgb(hex);
566
- const [h, s, l] = hexToHsl(hex);
567
- const [hh, ss, vv] = rgbToHsv({ r, g, b });
568
- return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
569
- }
566
+ // 8-digit (#rrggbbaa) or 4-digit (#rgba) — check before isHexColor
570
567
  const hex8 = str.match(/^#([0-9a-f]{8}|[0-9a-f]{4})$/i);
571
568
  if (hex8) {
572
569
  const rgba = hex8ToRgba(str);
@@ -578,6 +575,13 @@ export function normalizeColor(input) {
578
575
  return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
579
576
  }
580
577
  }
578
+ if (isHexColor(str)) {
579
+ const hex = normalizeHex(str);
580
+ const [r, g, b] = hexToRgb(hex);
581
+ const [h, s, l] = hexToHsl(hex);
582
+ const [hh, ss, vv] = rgbToHsv({ r, g, b });
583
+ return { type: 'hex', hex, r, g, b, a: 1, h, s, l, v: vv };
584
+ }
581
585
  if (isRgbColor(str)) {
582
586
  const rgba = rgbaStringToRgba(str);
583
587
  if (rgba) {
@@ -604,17 +608,6 @@ export function normalizeColor(input) {
604
608
  }
605
609
  if (str === 'transparent')
606
610
  return { type: 'named', hex: '#000000', r: 0, g: 0, b: 0, a: 0 };
607
- // 4-digit #RGBA hex
608
- if (/^#[0-9a-f]{4}$/i.test(str)) {
609
- const rgba = shortHexToRgba(str);
610
- if (rgba) {
611
- const { r, g, b, a } = rgba;
612
- const hex = rgbToHex({ r, g, b });
613
- const [h, s, l] = rgbToHsl({ r, g, b });
614
- const [, , vv] = rgbToHsv({ r, g, b });
615
- return { type: 'hex', hex, r, g, b, a, h, s, l, v: vv };
616
- }
617
- }
618
611
  // oklch / oklcha
619
612
  if (isOklchColor(str)) {
620
613
  const parsed = parseOklchString(str);
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "color-value-tools",
3
- "version": "1.1.4",
4
- "description": "A comprehensive utility library for parsing, converting, manipulating, and analyzing color values across all major color models — hex, RGB, HSL, HSV, HWB, Lab, LCH, OKLAB, OKLCH, CMYK plus CSS variables and named colors.",
3
+ "version": "1.1.6",
4
+ "description": "Parse, convert and manipulate colors across hex, RGB, HSL, Lab, LCH, OKLCH and CMYK, with WCAG contrast checks and CSS variables. Zero dependencies.",
5
+ "author": "macrulez <macrulezru@gmail.com> (https://macrulez.ru/en)",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/macrulezru/color-value-tools#readme",
5
8
  "repository": {
6
9
  "type": "git",
7
10
  "url": "git+https://github.com/macrulezru/color-value-tools.git"
8
11
  },
9
- "homepage": "https://macrulez.ru/#/en",
10
12
  "bugs": {
11
13
  "url": "https://github.com/macrulezru/color-value-tools/issues"
12
14
  },
@@ -15,18 +17,24 @@
15
17
  "types": "dist/cjs/index.d.ts",
16
18
  "exports": {
17
19
  ".": {
20
+ "types": "./dist/cjs/index.d.ts",
18
21
  "import": "./dist/esm/index.js",
19
- "require": "./dist/cjs/index.js",
20
- "types": "./dist/cjs/index.d.ts"
22
+ "require": "./dist/cjs/index.js"
21
23
  }
22
24
  },
23
25
  "bin": {
24
26
  "cvt": "./dist/cli/bin/cli.js"
25
27
  },
26
28
  "files": [
27
- "dist"
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
28
32
  ],
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
29
36
  "scripts": {
37
+ "prepublishOnly": "npm run build",
30
38
  "build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && tsc -p tsconfig.cli.json && node -e \"require('fs').writeFileSync('dist/esm/package.json', JSON.stringify({type:'module'}))\"",
31
39
  "test": "vitest run"
32
40
  },
@@ -36,19 +44,13 @@
36
44
  "hex",
37
45
  "rgb",
38
46
  "hsl",
39
- "hsv",
40
- "hwb",
41
- "oklab",
42
47
  "oklch",
43
48
  "cmyk",
44
- "lab",
45
- "lch",
49
+ "color-conversion",
46
50
  "wcag",
47
51
  "accessibility",
48
52
  "typescript"
49
53
  ],
50
- "author": "",
51
- "license": "MIT",
52
54
  "devDependencies": {
53
55
  "@types/node": "^25.5.0",
54
56
  "typescript": "^5.0.0",