chromonym 1.4.1 → 2.0.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.
Files changed (106) hide show
  1. package/NOTICE.md +32 -4
  2. package/README.md +216 -75
  3. package/dist/conversions/hex.d.ts +1 -1
  4. package/dist/conversions/hex.d.ts.map +1 -1
  5. package/dist/conversions/hex.js +2 -2
  6. package/dist/conversions/hex.js.map +1 -1
  7. package/dist/conversions/hsl.d.ts +1 -1
  8. package/dist/conversions/hsl.d.ts.map +1 -1
  9. package/dist/conversions/hsl.js +2 -2
  10. package/dist/conversions/hsl.js.map +1 -1
  11. package/dist/conversions/hsv.d.ts +1 -1
  12. package/dist/conversions/hsv.d.ts.map +1 -1
  13. package/dist/conversions/hsv.js +2 -2
  14. package/dist/conversions/hsv.js.map +1 -1
  15. package/dist/conversions/index.d.ts +5 -5
  16. package/dist/conversions/index.d.ts.map +1 -1
  17. package/dist/conversions/index.js +5 -5
  18. package/dist/conversions/index.js.map +1 -1
  19. package/dist/conversions/pantone.d.ts +9 -7
  20. package/dist/conversions/pantone.d.ts.map +1 -1
  21. package/dist/conversions/pantone.js +11 -10
  22. package/dist/conversions/pantone.js.map +1 -1
  23. package/dist/conversions/rgb.d.ts +1 -1
  24. package/dist/conversions/rgb.d.ts.map +1 -1
  25. package/dist/conversions/rgb.js +1 -1
  26. package/dist/conversions/rgb.js.map +1 -1
  27. package/dist/convert.d.ts +45 -5
  28. package/dist/convert.d.ts.map +1 -1
  29. package/dist/convert.js +68 -17
  30. package/dist/convert.js.map +1 -1
  31. package/dist/detectFormat.d.ts +1 -1
  32. package/dist/detectFormat.d.ts.map +1 -1
  33. package/dist/detectFormat.js +2 -6
  34. package/dist/detectFormat.js.map +1 -1
  35. package/dist/identify.d.ts +13 -16
  36. package/dist/identify.d.ts.map +1 -1
  37. package/dist/identify.js +12 -12
  38. package/dist/identify.js.map +1 -1
  39. package/dist/index.d.ts +14 -14
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +13 -13
  42. package/dist/index.js.map +1 -1
  43. package/dist/indexing.d.ts +30 -24
  44. package/dist/indexing.d.ts.map +1 -1
  45. package/dist/indexing.js +49 -22
  46. package/dist/indexing.js.map +1 -1
  47. package/dist/math/clamp.d.ts.map +1 -1
  48. package/dist/math/clamp.js.map +1 -1
  49. package/dist/math/colorSpace.d.ts +1 -1
  50. package/dist/math/colorSpace.d.ts.map +1 -1
  51. package/dist/math/colorSpace.js.map +1 -1
  52. package/dist/math/deltaE.d.ts.map +1 -1
  53. package/dist/math/deltaE.js.map +1 -1
  54. package/dist/math/euclideanDistance.d.ts +1 -1
  55. package/dist/math/euclideanDistance.d.ts.map +1 -1
  56. package/dist/math/euclideanDistance.js.map +1 -1
  57. package/dist/math/hueSector.d.ts.map +1 -1
  58. package/dist/math/hueSector.js.map +1 -1
  59. package/dist/palettes/index.d.ts +4 -0
  60. package/dist/palettes/index.d.ts.map +1 -0
  61. package/dist/palettes/index.js +4 -0
  62. package/dist/palettes/index.js.map +1 -0
  63. package/dist/palettes/normalize.d.ts +17 -0
  64. package/dist/palettes/normalize.d.ts.map +1 -0
  65. package/dist/palettes/normalize.js +19 -0
  66. package/dist/palettes/normalize.js.map +1 -0
  67. package/dist/palettes/pantone.d.ts +1834 -0
  68. package/dist/palettes/pantone.d.ts.map +1 -0
  69. package/dist/palettes/pantone.js +925 -0
  70. package/dist/palettes/pantone.js.map +1 -0
  71. package/dist/palettes/web.d.ts +320 -0
  72. package/dist/palettes/web.d.ts.map +1 -0
  73. package/dist/{colorspaces → palettes}/web.js +18 -3
  74. package/dist/palettes/web.js.map +1 -0
  75. package/dist/palettes/x11.d.ts +1346 -0
  76. package/dist/palettes/x11.d.ts.map +1 -0
  77. package/dist/palettes/x11.js +686 -0
  78. package/dist/palettes/x11.js.map +1 -0
  79. package/dist/resolve.d.ts +9 -7
  80. package/dist/resolve.d.ts.map +1 -1
  81. package/dist/resolve.js +18 -17
  82. package/dist/resolve.js.map +1 -1
  83. package/dist/types.d.ts +10 -6
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/types.js +2 -1
  86. package/dist/types.js.map +1 -1
  87. package/package.json +32 -18
  88. package/dist/colorspaces/index.d.ts +0 -4
  89. package/dist/colorspaces/index.d.ts.map +0 -1
  90. package/dist/colorspaces/index.js +0 -4
  91. package/dist/colorspaces/index.js.map +0 -1
  92. package/dist/colorspaces/pantone.d.ts +0 -919
  93. package/dist/colorspaces/pantone.d.ts.map +0 -1
  94. package/dist/colorspaces/pantone.js +0 -918
  95. package/dist/colorspaces/pantone.js.map +0 -1
  96. package/dist/colorspaces/registry.d.ts +0 -26
  97. package/dist/colorspaces/registry.d.ts.map +0 -1
  98. package/dist/colorspaces/registry.js +0 -36
  99. package/dist/colorspaces/registry.js.map +0 -1
  100. package/dist/colorspaces/web.d.ts +0 -156
  101. package/dist/colorspaces/web.d.ts.map +0 -1
  102. package/dist/colorspaces/web.js.map +0 -1
  103. package/dist/colorspaces/x11.d.ts +0 -667
  104. package/dist/colorspaces/x11.d.ts.map +0 -1
  105. package/dist/colorspaces/x11.js +0 -666
  106. package/dist/colorspaces/x11.js.map +0 -1
package/NOTICE.md CHANGED
@@ -6,7 +6,7 @@
6
6
  **not affiliated with, endorsed by, licensed by, or certified by Pantone
7
7
  LLC**.
8
8
 
9
- The `pantone` colorspace in this library ships **community-derived sRGB
9
+ The `pantone` palette in this library ships **community-derived sRGB
10
10
  approximations** of the Pantone Coated (C) color set, sourced from
11
11
  [`color_library`](https://github.com/draganradu/color_library) (MIT © Radu
12
12
  Dragan). These values:
@@ -16,21 +16,49 @@ Dragan). These values:
16
16
  exactly.
17
17
  - Are intended for on-screen reference, nearest-name identification, and
18
18
  general tooling — **not for print color specification**.
19
- - Use standard color codes (e.g. `185C`) as **nominative references** to
19
+ - Use standard color codes (e.g. `185 C`) as **nominative references** to
20
20
  publicly identified Pantone colors. No endorsement or claim of accuracy
21
21
  is implied.
22
22
 
23
+ ### Accuracy envelope
24
+
25
+ The approximations are closest for mid-chroma colors under a D65 / 2°
26
+ observer. As a rough guide:
27
+
28
+ - **Mid-chroma Pantones**: ΔE*00 ≲ 5 versus Pantone's own sRGB previews.
29
+ Usable for nearest-name identification and comparative swatch UI.
30
+ - **Saturated edges**: ΔE*00 commonly 5–10, higher in the deep-blue and
31
+ blue-violet region (~2736 C through ~2746 C). `identify` will still
32
+ converge on a plausible neighbor, but the *which* can depend on metric
33
+ choice — see the "Distance metrics" section of the README.
34
+ - **Fluorescents (801 C – 814 C)** and the **metallic series (8xxx C)**
35
+ are defined outside the sRGB gamut. Their sRGB approximations are
36
+ perceptually wrong by construction — the physical inks can't be
37
+ reproduced on a standard monitor. Do not use these values as a
38
+ reference for brand-critical matching; identify carefully or skip.
39
+ - **Neons / warm reds** clipping against the sRGB gamut boundary (Pantone
40
+ Red 032 / Warm Red / Red 021 area) are approximated via hard clip.
41
+
42
+ ### What `identify` does *not* guarantee
43
+
44
+ `identify(sampledRgba, { palette: pantone })` returns the nearest
45
+ entry **in the shipped sRGB approximation set**. It does **not** imply
46
+ that the physical ink bearing that Pantone code would match your sampled
47
+ color in a print workflow, under a different light source, or on
48
+ different paper stock. Pantone is a spectral specification; any
49
+ color-library lookup that rounds it through sRGB loses information.
50
+
23
51
  If you need Pantone-certified color values, consult Pantone directly:
24
52
  <https://www.pantone.com/>.
25
53
 
26
54
  ## X11 / X.Org
27
55
 
28
- The `x11` colorspace is derived from the X.Org `rgb.txt` file, which is in
56
+ The `x11` palette is derived from the X.Org `rgb.txt` file, which is in
29
57
  the public domain.
30
58
 
31
59
  ## CSS / SVG named colors
32
60
 
33
- The `web` colorspace mirrors the CSS Color Module Level 4 named-color
61
+ The `web` palette mirrors the CSS Color Module Level 4 named-color
34
62
  set, which is a public specification.
35
63
 
36
64
  ## Upstream dependencies
package/README.md CHANGED
@@ -11,18 +11,16 @@
11
11
  # chromonym
12
12
 
13
13
  [![npm version](https://img.shields.io/npm/v/chromonym?color=cb3837&logo=npm)](https://www.npmjs.com/package/chromonym)
14
- [![publish size](https://badgen.net/packagephobia/publish/chromonym?color=green)](https://packagephobia.com/result?p=chromonym)
15
- [![install size](https://badgen.net/packagephobia/install/chromonym?color=green)](https://packagephobia.com/result?p=chromonym)
16
14
  [![Types: included](https://img.shields.io/npm/types/chromonym?color=3178c6&logo=typescript)](https://www.npmjs.com/package/chromonym)
17
15
  [![CI](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml/badge.svg)](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml)
18
- [![codecov](https://codecov.io/gh/simiancraft/chromonym/branch/main/graph/badge.svg)](https://codecov.io/gh/simiancraft/chromonym)
19
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
+
17
+ <sub>[bundle size](https://packagephobia.com/result?p=chromonym) • [coverage](https://codecov.io/github/simiancraft/chromonym) • [MIT license](https://opensource.org/licenses/MIT)</sub>
20
18
 
21
19
  <p align="center">
22
20
  <code>identify</code> &nbsp;•&nbsp; <code>resolve</code> &nbsp;•&nbsp; <code>convert</code>
23
21
  </p>
24
22
 
25
- **Tree-shakeable color naming for TypeScript** — across CSS, X11, and Pantone, with six distance metrics (four perceptual).
23
+ **The last color-naming library you'll ever need.** One API — `identify`, `resolve`, `convert` — works against any palette you throw at it. Ships CSS, X11, and Pantone out of the box; bring your own (brand colors, paint chips, game factions, chart themes) with full TypeScript inference and zero registration. Perceptual accuracy via six distance metrics. Tree-shakes to the bone — you only pay for palettes you import.
26
24
 
27
25
  <p align="center">
28
26
  <a href="https://simiancraft.github.io/chromonym/">
@@ -30,31 +28,45 @@
30
28
  </a>
31
29
  </p>
32
30
 
33
- Because "it's sort of magenta-ish, maybe?" doesn't copy-paste into code, and the nearest Pantone to your brand hex is something you should be able to answer without opening a swatch book.
34
-
35
- The identification and resolution mechanism is the same across every colorspace, so future additions (RAL, HKS, NCS, or similar) are one file each. There's no public `registerColorspace` API yet — open an issue if you need a specific set.
31
+ Because "it's sort of magenta-ish, maybe?" doesn't copy-paste into code — and naming a color is the same problem whether your palette is CSS, Pantone, your company's brand kit, or a homebrew set of faction colors. Chromonym treats every palette as a first-class `Palette<Name>` object and runs the same nearest-neighbor machinery over all of them.
36
32
 
37
33
  For color *manipulation* (mixing, scales, gamut mapping), reach for [`chroma-js`](https://gka.github.io/chroma.js/) or [`color.js`](https://colorjs.io/) — chromonym is the tool for *naming*.
38
34
 
39
35
  ```ts
40
- import { identify, resolve, convert } from 'chromonym';
41
-
42
- // What Pantone is this brand color closest to? (perceptually, via CIEDE2000)
43
- identify('#E20074', { colorspace: 'pantone' }) // '213C' — T-Mobile magenta
44
-
45
- // Resolve any Pantone code back to RGB — prefix, spacing, case all fine
46
- resolve('Pantone 185 C', { colorspace: 'pantone' })// '#e4002b'
36
+ import { identify, resolve, convert, pantone, type Palette } from 'chromonym';
37
+
38
+ // Your palette — defined inline, type-checked, no registration. Works the
39
+ // same as the built-in ones because that's the whole point.
40
+ const brand = {
41
+ name: 'acme-brand',
42
+ colors: { 'acme red': '#ff2a3b', 'acme ink': '#0a0f2c', 'acme mist': '#e6ecf5' },
43
+ normalize: (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
44
+ defaultMetric: 'deltaEok',
45
+ } as const satisfies Palette;
46
+
47
+ identify('#ff2b3c', { palette: brand }) // 'acme red'
48
+ resolve('Acme Ink', { palette: brand }) // '#0a0f2c'
49
+
50
+ // Built-in palettes follow the identical shape — CSS, X11, Pantone out of the box.
51
+ identify('#ff8080') // 'lightcoral' (web is the default)
52
+ identify('#663399') // 'rebeccapurple'
53
+ identify('#E20074', { palette: pantone }) // '213 C' — T-Mobile magenta
54
+ resolve('Pantone 185 C', { palette: pantone }) // '#e4002b'
47
55
 
48
- // Format conversion, format-detecting input
56
+ // Format conversion: HEX / RGB / RGBA / HSL / HSV (palette-free, tree-shakes cleanly)
49
57
  convert('#ff0000', { format: 'HSL' }) // 'hsl(0, 100%, 50%)'
50
58
  convert({ h: 120, s: 100, l: 50 }, { format: 'HEX' }) // '#00ff00'
51
59
 
52
- // The classic: nearest CSS name
53
- identify('#ff8080') // 'lightcoral'
54
- identify('#bada55') // 'yellowgreen' — also the best-spelled hex
55
- identify('#663399') // 'rebeccapurple'
60
+ // Same convert API understands palette names both ways when you pass one:
61
+ // name in → any structural format out; color in → exact palette key out.
62
+ // `'NAME'` is an exact reverse lookup (throws on miss; use `identify` for fuzzy).
63
+ convert('185 C', { palette: pantone, format: 'HSL' }) // 'hsl(349, 100%, 45%)'
64
+ convert('#e4002b', { palette: pantone, format: 'NAME' }) // '185 C' (exact match)
65
+ convert('acme red', { palette: brand, format: 'RGB' }) // 'rgb(255, 42, 59)'
56
66
  ```
57
67
 
68
+ The identification machinery is palette-agnostic — RAL, HKS, NCS, Munsell, or a bespoke in-house set are the same amount of work (a small file each, or a single object literal). PRs welcome; meanwhile, BYO lets you ship today.
69
+
58
70
  ## Install
59
71
 
60
72
  ```sh
@@ -70,53 +82,58 @@ Three primary functions. All accept an optional `opts` object; defaults fire whe
70
82
 
71
83
  ### `identify(input, opts?)`
72
84
 
73
- **Color → name.** Finds the nearest-match name in the chosen colorspace using the selected perceptual distance metric.
85
+ **Color → name.** Finds the nearest-match name in the chosen palette using the selected perceptual distance metric.
74
86
 
75
87
  ```ts
76
- function identify<C extends ColorspaceName = 'web'>(
88
+ function identify<C extends Palette = typeof web>(
77
89
  input: ColorInput,
78
- opts?: { colorspace?: C; metric?: DistanceMetric },
79
- ): WebColorName | X11ColorName | PantoneColorName | null
90
+ opts?: { palette?: C; metric?: DistanceMetric },
91
+ ): Extract<keyof C['colors'], string> | null
80
92
  ```
81
93
 
82
- - **Default colorspace**: `'web'`.
83
- - **Default metric**: picked per colorspace — `'deltaE76'` for web and x11 (well-separated palettes), `'deltaE2000'` for pantone (dense, perceptually-packed). Override freely via `metric`.
94
+ - **Default palette**: the built-in `web` palette (148 entries, small — included if you call `identify` without a palette).
95
+ - **Default metric**: read from the palette's own `defaultMetric` — `'deltaE76'` for web and x11 (well-separated palettes), `'deltaE2000'` for pantone (dense, perceptually-packed). Override freely via `metric`.
84
96
  - Returns the matched name, or `null` if input is unrecognized (`detectFormat` returns `'UNKNOWN'`).
85
- - Return type narrows based on the `colorspace` option — e.g. `identify(hex, { colorspace: 'pantone' })` returns `PantoneColorName | null`, and the no-args call returns `WebColorName | null`.
97
+ - Return type is inferred from the palette's `colors` keys — so passing `{ palette: pantone }` narrows to `PantoneColorName | null`, and a BYO palette with a literal key union narrows to that union.
86
98
  - Ties go to whichever color was defined first — deterministic across runs, but not semantically meaningful.
87
99
 
88
100
  ```ts
89
- identify('#ff0000') // 'red'
90
- identify([255, 0, 0]) // 'red'
91
- identify({ r: 250, g: 20, b: 60 }) // 'crimson' (nearest)
92
- identify('#ff0000', { colorspace: 'x11' }) // 'red' (or 'red1', X11 has numbered variants)
93
- identify('#ff0000', { colorspace: 'pantone' }) // nearest Pantone C code, e.g. '185C'
94
- identify('#ff0080', { metric: 'deltaE2000' }) // force perceptual-accurate match
95
- identify('#ff0000', { metric: 'euclidean-srgb' }) // force fastest (non-perceptual) match
101
+ import { identify, pantone, x11 } from 'chromonym';
102
+
103
+ identify('#ff0000') // 'red' (web default)
104
+ identify([255, 0, 0]) // 'red'
105
+ identify({ r: 250, g: 20, b: 60 }) // 'crimson' (nearest)
106
+ identify('#ff0000', { palette: x11 }) // 'red' (x11 also has 'red 1' through 'red 4')
107
+ identify('#e4002b', { palette: pantone }) // '185 C' (exact match — Target/Coca-Cola red)
108
+ identify('#ff0000', { palette: pantone }) // '172 C' (nearest — pure red isn't a Pantone)
109
+ identify('#ff0080', { metric: 'deltaE2000' }) // force perceptual-accurate match
110
+ identify('#ff0000', { metric: 'euclidean-srgb' }) // force fastest (non-perceptual) match
96
111
  ```
97
112
 
98
113
  ### `resolve(name, opts?)`
99
114
 
100
- **Name → color.** Normalizes the input (lowercases it, strips all non-alphanumeric characters), looks up the canonical key in the chosen colorspace, and returns the value in the chosen format.
115
+ **Name → color.** Normalizes the input (lowercases it, strips all non-alphanumeric characters), looks up the canonical key in the chosen palette, and returns the value in the chosen format.
101
116
 
102
117
  ```ts
103
118
  function resolve(
104
119
  name: string,
105
- opts?: { colorspace?: ColorspaceName; format?: ColorFormat },
120
+ opts?: { palette?: Palette; format?: ColorFormat },
106
121
  ): ColorValue | null
107
122
  ```
108
123
 
109
- - **Default colorspace**: `'web'`.
124
+ - **Default palette**: the built-in `web` palette.
110
125
  - **Default format**: `'HEX'`.
111
- - Returns `null` if the normalized name isn't in the colorspace.
112
- - Normalization lets users pass `'Alice Blue'`, `'alice-blue'`, `'ALICEBLUE'`, `'Pantone 185 C'` etc. — all resolved identically.
126
+ - Returns `null` if the normalized name isn't in the palette.
127
+ - Normalization is the palette's own `normalize` function — web/x11 lowercase + strip non-alphanumeric, pantone also strips a `Pantone` / `PMS` prefix. So `'Alice Blue'`, `'alice-blue'`, `'ALICEBLUE'`, `'Pantone 185 C'` all resolve identically in their respective palettes.
113
128
 
114
129
  ```ts
130
+ import { resolve, pantone } from 'chromonym';
131
+
115
132
  resolve('crimson') // '#dc143c'
116
133
  resolve('Alice Blue') // '#f0f8ff'
117
134
  resolve('alice-blue!') // '#f0f8ff'
118
- resolve('185 C', { colorspace: 'pantone' }) // '#e4002b'
119
- resolve('Pantone 185 C', { colorspace: 'pantone' })// '#e4002b' (same, normalized)
135
+ resolve('185 C', { palette: pantone }) // '#e4002b'
136
+ resolve('Pantone 185 C', { palette: pantone }) // '#e4002b' (same, normalized)
120
137
  resolve('crimson', { format: 'RGB' }) // 'rgb(220, 20, 60)'
121
138
  resolve('crimson', { format: 'RGBA' }) // { r: 220, g: 20, b: 60, a: 1 }
122
139
  resolve('not-a-color') // null
@@ -124,52 +141,169 @@ resolve('not-a-color') // null
124
141
 
125
142
  ### `convert(input, opts?)`
126
143
 
127
- **Color → color in a different format.** Detects the input format, normalizes to the canonical internal `Rgba` representation, and returns the value in the target format. Colorspace-independent.
144
+ **Color → color in a different format.** Detects the input format, normalizes to the canonical internal `Rgba`, and returns the value in the target format.
145
+
146
+ Structural on its own (HEX ↔ RGB ↔ RGBA ↔ HSL ↔ HSV, no palette data bundled). Pass an optional `palette` and the same one API understands palette names both ways — input as a name, or `format: 'NAME'` output for exact reverse lookup.
128
147
 
129
148
  ```ts
130
- function convert(
131
- input: ColorInput,
132
- opts?: { format?: ColorFormat },
133
- ): ColorValue
149
+ // Structural (palette-free, tree-shakes to ~5 KB min)
150
+ function convert(input: ColorInput, opts?: { format?: ColorFormat }): ColorValue;
151
+
152
+ // With a palette — input can be a palette name, output can be 'NAME' (exact key)
153
+ function convert<P extends Palette>(
154
+ input: ColorInput | string,
155
+ opts: { palette: P; format: 'NAME' },
156
+ ): PaletteKey<P>;
157
+ function convert<P extends Palette>(
158
+ input: ColorInput | string,
159
+ opts: { palette: P; format?: ColorFormat },
160
+ ): ColorValue;
134
161
  ```
135
162
 
136
163
  - **Default format**: `'HEX'`.
137
- - Throws if input format is unrecognized.
138
- - Alpha information is preserved when converting into RGBA/RGB(A) string forms, and discarded for HEX (6-digit) / HSL / HSV / PANTONE output.
164
+ - Throws on unrecognized input (parser-flavored — `identify` / `resolve` return `null` instead).
165
+ - Alpha is preserved only in `'RGBA'` output (canonical `{ r, g, b, a }` object); dropped by `'HEX'` (6-digit), `'RGB'`, `'HSL'`, `'HSV'`, and `'NAME'`.
166
+ - **Palette is caller-supplied**: no palette is pulled unless you import one and pass it. Tree-shake stays intact — `convert`-only bundles include zero palette data.
167
+ - `format: 'NAME'` is **exact-match only**; it throws if the input isn't a pixel-exact palette entry. Use `identify` for nearest-match.
168
+ - Priority: if the input is structurally valid (hex, rgb, hsl, …), that wins over palette-name lookup. So `convert('#ff0000', { palette: brand })` always means "parse as hex."
169
+
170
+ ```ts
171
+ // Structural — no palette imported, nothing bundled
172
+ convert('#ff0000') // '#ff0000' (identity)
173
+ convert('#ff0000', { format: 'RGB' }) // 'rgb(255, 0, 0)'
174
+ convert([255, 0, 0], { format: 'HEX' }) // '#ff0000'
175
+ convert({ h: 0, s: 100, l: 50 }, { format: 'HEX' }) // '#ff0000'
176
+
177
+ // With a palette — palette-name in, any format out
178
+ import { pantone } from 'chromonym';
179
+ convert('185 C', { palette: pantone }) // '#e4002b'
180
+ convert('Pantone 185 C', { palette: pantone }) // '#e4002b' (normalizer strips prefix)
181
+ convert('185 C', { palette: pantone, format: 'HSL' }) // 'hsl(349, 100%, 45%)'
182
+
183
+ // With a palette — color in, exact palette key out
184
+ convert('#e4002b', { palette: pantone, format: 'NAME' }) // '185 C'
185
+ convert('#663399', { palette: web, format: 'NAME' }) // 'rebeccapurple'
186
+ convert('#ff0000', { palette: pantone, format: 'NAME' }) // throws — use identify for nearest
187
+
188
+ // BYO palettes work the same way
189
+ convert('acme red', { palette: brand, format: 'RGB' }) // 'rgb(255, 42, 59)'
190
+ convert('#ff2a3b', { palette: brand, format: 'NAME' }) // 'acme red'
191
+ ```
192
+
193
+ The low-level `pantoneToRgba` / `rgbaToPantone` functions (from `chromonym` or `chromonym/conversions/pantone`) remain available for single-purpose Pantone work when you don't need the full `convert` ceremony.
194
+
195
+ ## Palettes
196
+
197
+ Every palette — yours or ours — is a `Palette<Name>` object. Define it once, pass it anywhere the three verbs accept one.
198
+
199
+ ```ts
200
+ type Palette<Name extends string = string> = {
201
+ readonly name: string; // human-readable label
202
+ readonly colors: Record<Name, HexColor>; // the lookup data
203
+ readonly normalize: (s: string) => string; // user-input → canonical key
204
+ readonly defaultMetric: DistanceMetric; // used by identify when no metric override
205
+ };
206
+ ```
207
+
208
+ ### Bring your own
209
+
210
+ The primary shape. Any object matching `Palette<Name>` works — no registry, no plugin, no side effects. Bundlers only include the palettes you actually import.
139
211
 
140
212
  ```ts
141
- convert('#ff0000') // '#ff0000' (identity)
142
- convert('#ff0000', { format: 'RGB' }) // 'rgb(255, 0, 0)'
143
- convert('#ff0000', { format: 'RGBA' }) // { r: 255, g: 0, b: 0, a: 1 }
144
- convert([255, 0, 0], { format: 'HEX' }) // '#ff0000'
145
- convert('rgb(255, 0, 0)', { format: 'HSL' }) // 'hsl(0, 100%, 50%)'
146
- convert({ h: 0, s: 100, l: 50 }, { format: 'HEX' })// '#ff0000'
147
- convert('185 C', { format: 'HEX' }) // '#e4002b'
148
- convert('#e4002b', { format: 'PANTONE' }) // '185C' (nearest Pantone C)
213
+ import { identify, resolve, type Palette } from 'chromonym';
214
+
215
+ const warhammer = {
216
+ name: 'warhammer40k',
217
+ colors: {
218
+ 'world eaters red': '#8b1a1a',
219
+ 'adeptus red': '#652022',
220
+ 'sons of malice white': '#e8e4d8',
221
+ 'the flawless host purple': '#6b2d7d',
222
+ 'nurgle green': '#748c3f',
223
+ 'alpha legion teal': '#2a6d7a',
224
+ },
225
+ normalize: (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
226
+ defaultMetric: 'deltaE2000',
227
+ } as const satisfies Palette;
228
+
229
+ identify('#750c0c', { palette: warhammer })
230
+ // → 'world eaters red' (return type narrows to the palette's key union)
231
+
232
+ resolve('Nurgle Green', { palette: warhammer })
233
+ // → '#748c3f' (your normalizer handles case + punctuation)
149
234
  ```
150
235
 
151
- ## Colorspaces
236
+ ### The three we ship
152
237
 
153
238
  | Name | Entries | Source |
154
239
  |---|---|---|
155
- | `'web'` (default) | 148 | CSS Color Module Level 4 named colors |
156
- | `'x11'` | 658 | X.Org `rgb.txt` (public domain) |
157
- | `'pantone'` | 907 | Pantone Coated (C) — community approximations (not Pantone-licensed) |
240
+ | `web` (default) | 148 | CSS Color Module Level 4 named colors |
241
+ | `x11` | 658 | X.Org `rgb.txt` (public domain) |
242
+ | `pantone` | 907 | Pantone Coated (C) — community approximations (not Pantone-licensed) |
158
243
 
159
- Each colorspace is a pure data object: `Record<string, HexColor>`. Importable directly:
244
+ Importable directly, or via subpath exports for stricter tree-shaking:
160
245
 
161
246
  ```ts
162
247
  import { web, x11, pantone } from 'chromonym';
163
- web.crimson // '#dc143c'
248
+ // or: import { pantone } from 'chromonym/pantone';
249
+
250
+ web.colors.crimson // '#dc143c'
251
+ web.colors.aliceblue // '#f0f8ff' (CSS keyword form — paste-compatible)
252
+ pantone.colors['185 C'] // '#e4002b'
164
253
  ```
165
254
 
166
- *Trivia:* `web.rebeccapurple` (`#663399`) entered CSS Color 4 in 2014 in memory of [Rebecca Meyer](https://meyerweb.com/eric/thoughts/2014/06/19/rebeccapurple/). The X11 set ships every gray name twice (`gray`, `grey`) plus numbered variants up to 99, so `x11.gray73` is a real key (`#bababa`). Yes, these are Greys. No, not the kind that visit during sleep paralysis.
255
+ Each palette's keys follow its own domain convention — so `identify`'s output pastes back into whatever ecosystem the name came from. `web` uses the CSS spec's single-word form (`rebeccapurple`), `x11` uses the X.Org docs' spaced form (`'antique white 1'`), `pantone` uses the official print notation (`'185 C'`).
256
+
257
+ *Trivia:* `web.colors.rebeccapurple` (`#663399`) entered CSS Color 4 in 2014 in memory of [Rebecca Meyer](https://meyerweb.com/eric/thoughts/2014/06/19/rebeccapurple/). The X11 set ships every gray name twice (`gray`, `grey`) plus numbered variants up to 99, so `x11.colors['gray 73']` is a real key (`#bababa`). Yes, these are Greys. No, not the kind that visit during sleep paralysis.
258
+
259
+ ### Cross-palette translation
260
+
261
+ "What's this CSS color in X11?" / "What's our brand red in Pantone?" is a *nearest-match* question — palettes almost never share exact hex values, so there's no strict round-trip. That's `identify`'s job. Chain `convert` (strict name → hex) with `identify` (fuzzy hex → nearest name):
262
+
263
+ ```ts
264
+ import { convert, identify, web, x11, pantone } from 'chromonym';
265
+
266
+ // web → x11
267
+ identify(convert('rebeccapurple', { palette: web }), { palette: x11 })
268
+ // → 'dark orchid 4'
269
+
270
+ identify(convert('dodgerblue', { palette: web }), { palette: x11 })
271
+ // → 'dodger blue' (hex shared across both palettes — exact hit)
272
+
273
+ // x11 → pantone
274
+ identify(convert('dark slate gray 4', { palette: x11 }), { palette: pantone })
275
+ // → '5483 C'
276
+
277
+ identify(convert('debian red', { palette: x11 }), { palette: pantone })
278
+ // → '1925 C'
279
+
280
+ // web → pantone (the classic brand-matching case)
281
+ identify(convert('hotpink', { palette: web }), { palette: pantone }) // → '812 C'
282
+ identify(convert('dodgerblue', { palette: web }), { palette: pantone }) // → '279 C'
283
+
284
+ // BYO → pantone (ship your brand palette, find the closest Pantone for print)
285
+ identify(convert('acme red', { palette: brand }), { palette: pantone }) // → '1788 C'
286
+
287
+ // Override the metric for the *nearest-match* leg if you want — input parsing
288
+ // is always exact, so only the second call takes `metric`.
289
+ identify(convert('rebeccapurple', { palette: web }), { palette: pantone, metric: 'deltaEok' })
290
+ // → '526 C' (OKLAB disagrees with ΔE2000's '267 C' in the saturated-purple region)
291
+ ```
292
+
293
+ Why two calls instead of one: `convert` is the strict verb (throws on a name miss), `identify` is the fuzzy one (always returns a nearest). Keeping them separate lets you decide — per call — whether you want a hard error on an unknown input or a best-effort neighbor.
294
+
295
+ If this chain shows up often in your code, wrap it in your own helper:
296
+
297
+ ```ts
298
+ const translate = (name: string, from: Palette, to: Palette) =>
299
+ identify(convert(name, { palette: from }), { palette: to });
300
+ ```
167
301
 
168
302
  ## Distance metrics
169
303
 
170
304
  *Six ways to ask "how different are two colors?"*
171
305
 
172
- `identify` picks the nearest-named color by comparing your input to every entry in the chosen colorspace. *How* it measures "nearest" is controlled by `metric`. Six options, roughly ordered from fast-and-approximate to slow-and-correct:
306
+ `identify` picks the nearest-named color by comparing your input to every entry in the chosen palette. *How* it measures "nearest" is controlled by `metric`. Six options, roughly ordered from fast-and-approximate to slow-and-correct:
173
307
 
174
308
  | Metric | What it does | When to use |
175
309
  |---|---|---|
@@ -182,18 +316,20 @@ web.crimson // '#dc143c'
182
316
 
183
317
  Each metric trades cost for accuracy — the full scan over 907 Pantone entries is well under 1 ms even with ΔE2000. For interactive UIs, cost is usually irrelevant; drop to a cheaper metric when batch-processing millions of colors or when you need specific tie-breaking behavior.
184
318
 
319
+ > **Note on x11's default (ΔE76):** x11 has 658 entries, many of them saturated blue/purple ramps where ΔE76 is a known weak spot. The default optimizes for UI scrubbing speed (~5 µs vs ~90 µs for ΔE2000). If perceptual accuracy matters more than latency, override per call with `{ metric: 'deltaEok' }` — same cost as ΔE76 on cached Lab/OKLAB indexes, better separation in blues.
320
+
185
321
  ```ts
186
- // Defaults: deltaE76 for web/x11, deltaE2000 for pantone.
322
+ // Defaults: read from the palette's own `defaultMetric`.
187
323
  identify('#ff8080') // deltaE76 (web default)
188
- identify('#ff8080', { colorspace: 'pantone' }) // deltaE2000 (pantone default)
324
+ identify('#ff8080', { palette: pantone }) // deltaE2000 (pantone default)
189
325
 
190
326
  // Override per-call:
191
327
  identify('#ff8080', { metric: 'euclidean-srgb' }) // fastest
192
328
  identify('#ff8080', { metric: 'deltaE2000' }) // most accurate
193
- identify('#ff8080', { colorspace: 'pantone', metric: 'euclidean-srgb' }) // force fast for Pantone
329
+ identify('#ff8080', { palette: pantone, metric: 'euclidean-srgb' }) // force fast for Pantone
194
330
  ```
195
331
 
196
- The low-level `rgbaToPantone` always uses `'deltaE2000'`.
332
+ The low-level `rgbaToPantone` reads `pantone.defaultMetric` (currently `'deltaE2000'`).
197
333
 
198
334
  ## Formats
199
335
 
@@ -204,7 +340,6 @@ The low-level `rgbaToPantone` always uses `'deltaE2000'`.
204
340
  | `'RGBA'` | `'rgba(r, g, b, a)'`, `[r, g, b, a]`, `{ r, g, b, a }` | `{ r, g, b, a }` object |
205
341
  | `'HSL'` | `'hsl(h, s%, l%)'`, `{ h, s, l }` | `'hsl(h, s%, l%)'` string |
206
342
  | `'HSV'` | `'hsv(h, s%, v%)'`, `{ h, s, v }` | `'hsv(h, s%, v%)'` string |
207
- | `'PANTONE'` | `'185 C'`, `'185C'`, `'Pantone 185 C'`, etc. | `'185C'`-style string |
208
343
 
209
344
  ## Error handling
210
345
 
@@ -229,6 +364,7 @@ const tryConvert = (input: ColorInput, opts = {}) => {
229
364
  ## Utilities
230
365
 
231
366
  - **`detectFormat(input): DetectedFormat`** — runtime format detection. Returns one of the format keys or `'UNKNOWN'`.
367
+ - **`isColor(input): boolean`** — type-guard wrapper around `detectFormat`. Returns `true` if the input is a recognized structural color shape.
232
368
  - **`COLOR_FORMATS: ReadonlySet<ColorFormat>`** — the Set of valid format keys.
233
369
  - **Per-format converters** (tree-shakeable) — `hexToRgba`, `rgbaToHex`, `rgbToRgba`, `rgbaToRgb`, `hslToRgba`, `rgbaToHsl`, `hsvToRgba`, `rgbaToHsv`, `pantoneToRgba`, `rgbaToPantone`. Use these directly if you only need one conversion and want maximum tree-shaking.
234
370
 
@@ -237,7 +373,9 @@ const tryConvert = (input: ColorInput, opts = {}) => {
237
373
  - `sideEffects: false` in `package.json`.
238
374
  - All exports are named; no default export.
239
375
  - Barrel uses explicit re-exports.
240
- - Importing `{ resolve }` or `{ identify }` pulls in all three colorspaces (necessary for the string-keyed `colorspace` option). For strict tree-shaking of colorspace data, use the low-level converters and colorspace objects directly.
376
+ - `identify` / `resolve` take the palette **as an object**, not a string key. You only pay for palettes you actually import: calling `identify(hex)` with no override pulls in `web` (148 entries) and nothing else; passing `{ palette: pantone }` adds pantone. X11 and pantone are *not* bundled unless you reference them.
377
+ - BYO palettes have zero library cost beyond your own data.
378
+ - Subpath exports (`chromonym/web`, `chromonym/x11`, `chromonym/pantone`, `chromonym/conversions/hex`, `chromonym/math/deltaE`, etc.) let you import a single palette or converter without going through the root barrel.
241
379
 
242
380
  ## Types
243
381
 
@@ -246,9 +384,10 @@ Re-exported from the root barrel — `import type { ... } from 'chromonym'`:
246
384
  | Category | Types |
247
385
  |---|---|
248
386
  | Input / output unions | `ColorInput`, `ColorValue` |
249
- | Format & colorspace keys | `ColorFormat`, `ColorspaceName` |
250
- | Per-format shapes | `HexColor`, `Rgba`, `RgbInput`, `RgbaInput`, `HslInput`, `HsvInput`, `PantoneCode` |
251
- | Palette container | `Colorspace` |
387
+ | Format keys | `ColorFormat` |
388
+ | Per-format shapes | `HexColor`, `Rgba`, `RgbInput`, `RgbaInput`, `HslInput`, `HsvInput` |
389
+ | Palette container | `Palette<Name>`, `NormalizeFn` |
390
+ | Distance selector | `DistanceMetric` |
252
391
  | Color-name unions | `WebColorName`, `X11ColorName`, `PantoneColorName` |
253
392
 
254
393
  ## Development
@@ -261,7 +400,7 @@ bun test
261
400
  bun run build
262
401
  ```
263
402
 
264
- Regenerating colorspace data:
403
+ Regenerating palette data:
265
404
 
266
405
  ```sh
267
406
  bun run scripts/generate-x11.ts # reads /usr/share/X11/rgb.txt
@@ -284,8 +423,10 @@ chromonym deliberately limits its scope to color *naming* (resolving and identif
284
423
 
285
424
  MIT © [the-simian](https://github.com/the-simian)
286
425
 
426
+ <p align="center"><sub>Crafted with care by <a href="https://simiancraft.com">Simiancraft</a>.</sub></p>
427
+
287
428
  ---
288
429
 
289
430
  > ### Pantone® trademark notice
290
431
  >
291
- > **Pantone®** is a registered trademark of **Pantone LLC**. Chromonym is **not affiliated with, endorsed by, or certified by Pantone LLC**. The `pantone` colorspace ships **community-derived sRGB approximations** of the Pantone Coated (C) set. Values will **not** match a licensed Pantone reference exactly and are unsuitable for print color specification. See [`NOTICE.md`](./NOTICE.md) for full text.
432
+ > **Pantone®** is a registered trademark of **Pantone LLC**. Chromonym is **not affiliated with, endorsed by, or certified by Pantone LLC**. The `pantone` palette ships **community-derived sRGB approximations** of the Pantone Coated (C) set. Values will **not** match a licensed Pantone reference exactly and are unsuitable for print color specification. See [`NOTICE.md`](./NOTICE.md) for full text.
@@ -1,4 +1,4 @@
1
- import type { HexColor, Rgba } from '../types';
1
+ import type { HexColor, Rgba } from '../types.js';
2
2
  /**
3
3
  * Parse a hex color string into the canonical Rgba representation.
4
4
  * Accepts 3-digit (`#rgb`), 6-digit (`#rrggbb`), and 8-digit (`#rrggbbaa`).
@@ -1 +1 @@
1
- {"version":3,"file":"hex.d.ts","sourceRoot":"","sources":["../../src/conversions/hex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAsB7C;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,CAE9C"}
1
+ {"version":3,"file":"hex.d.ts","sourceRoot":"","sources":["../../src/conversions/hex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAElD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAsB7C;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,CAE9C","sourcesContent":["import { HEX_RE } from '../detectFormat.js';\nimport { clamp } from '../math/clamp.js';\nimport type { HexColor, Rgba } from '../types.js';\n\n/**\n * Parse a hex color string into the canonical Rgba representation.\n * Accepts 3-digit (`#rgb`), 6-digit (`#rrggbb`), and 8-digit (`#rrggbbaa`).\n * Throws on any other input shape.\n */\nexport function hexToRgba(hex: HexColor): Rgba {\n const match = HEX_RE.exec(hex);\n const body = match?.[1];\n if (!body) throw new Error(`Invalid hex color: ${hex}`);\n\n if (body.length === 3) {\n const r = body.charAt(0);\n const g = body.charAt(1);\n const b = body.charAt(2);\n return {\n r: parseInt(r + r, 16),\n g: parseInt(g + g, 16),\n b: parseInt(b + b, 16),\n a: 1,\n };\n }\n\n const r = parseInt(body.slice(0, 2), 16);\n const g = parseInt(body.slice(2, 4), 16);\n const b = parseInt(body.slice(4, 6), 16);\n const a = body.length === 8 ? parseInt(body.slice(6, 8), 16) / 255 : 1;\n return { r, g, b, a };\n}\n\nfunction channelToHex(n: number): string {\n return clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0');\n}\n\n/**\n * Emit a 6-digit lowercase hex string. Alpha is dropped; out-of-range\n * channel values are clamped to 0..255; fractional values are rounded.\n */\nexport function rgbaToHex(rgba: Rgba): HexColor {\n return `#${channelToHex(rgba.r)}${channelToHex(rgba.g)}${channelToHex(rgba.b)}` as HexColor;\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { HEX_RE } from '../detectFormat';
2
- import { clamp } from '../math/clamp';
1
+ import { HEX_RE } from '../detectFormat.js';
2
+ import { clamp } from '../math/clamp.js';
3
3
  /**
4
4
  * Parse a hex color string into the canonical Rgba representation.
5
5
  * Accepts 3-digit (`#rgb`), 6-digit (`#rrggbb`), and 8-digit (`#rrggbbaa`).
@@ -1 +1 @@
1
- {"version":3,"file":"hex.js","sourceRoot":"","sources":["../../src/conversions/hex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGtC;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAa;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,EAAE,CAAC,CAAC;IAExD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,OAAO;YACL,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,CAAC;SACL,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AACxB,CAAC;AAED,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAc,CAAC;AAC9F,CAAC"}
1
+ {"version":3,"file":"hex.js","sourceRoot":"","sources":["../../src/conversions/hex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGzC;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAa;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,EAAE,CAAC,CAAC;IAExD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,OAAO;YACL,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtB,CAAC,EAAE,CAAC;SACL,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AACxB,CAAC;AAED,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAc,CAAC;AAC9F,CAAC","sourcesContent":["import { HEX_RE } from '../detectFormat.js';\nimport { clamp } from '../math/clamp.js';\nimport type { HexColor, Rgba } from '../types.js';\n\n/**\n * Parse a hex color string into the canonical Rgba representation.\n * Accepts 3-digit (`#rgb`), 6-digit (`#rrggbb`), and 8-digit (`#rrggbbaa`).\n * Throws on any other input shape.\n */\nexport function hexToRgba(hex: HexColor): Rgba {\n const match = HEX_RE.exec(hex);\n const body = match?.[1];\n if (!body) throw new Error(`Invalid hex color: ${hex}`);\n\n if (body.length === 3) {\n const r = body.charAt(0);\n const g = body.charAt(1);\n const b = body.charAt(2);\n return {\n r: parseInt(r + r, 16),\n g: parseInt(g + g, 16),\n b: parseInt(b + b, 16),\n a: 1,\n };\n }\n\n const r = parseInt(body.slice(0, 2), 16);\n const g = parseInt(body.slice(2, 4), 16);\n const b = parseInt(body.slice(4, 6), 16);\n const a = body.length === 8 ? parseInt(body.slice(6, 8), 16) / 255 : 1;\n return { r, g, b, a };\n}\n\nfunction channelToHex(n: number): string {\n return clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0');\n}\n\n/**\n * Emit a 6-digit lowercase hex string. Alpha is dropped; out-of-range\n * channel values are clamped to 0..255; fractional values are rounded.\n */\nexport function rgbaToHex(rgba: Rgba): HexColor {\n return `#${channelToHex(rgba.r)}${channelToHex(rgba.g)}${channelToHex(rgba.b)}` as HexColor;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { HslInput, HslString, Rgba } from '../types';
1
+ import type { HslInput, HslString, Rgba } from '../types.js';
2
2
  /**
3
3
  * Parse HSL input (string or object) into canonical Rgba.
4
4
  * Hue wraps modulo 360; saturation and lightness are 0..100 percent.
@@ -1 +1 @@
1
- {"version":3,"file":"hsl.d.ts","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAK1D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAqC/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAyB/C"}
1
+ {"version":3,"file":"hsl.d.ts","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAK7D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAqC/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAyB/C","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HslInput, HslString, Rgba } from '../types.js';\n\nconst HSL_RE =\n /^hsl\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSL input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and lightness are 0..100 percent.\n */\nexport function hslToRgba(input: HslInput): Rgba {\n let h: number;\n let s: number;\n let l: number;\n\n if (typeof input === 'string') {\n const match = HSL_RE.exec(input);\n if (!match) throw new Error(`Invalid hsl string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n l = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n l = requireFinite(input.l, 'l');\n }\n\n const saturation = s / 100;\n const lightness = l / 100;\n\n if (saturation === 0) {\n const gray = Math.round(lightness * 255);\n return { r: gray, g: gray, b: gray, a: 1 };\n }\n\n const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const lightnessOffset = lightness - chroma / 2;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + lightnessOffset) * 255),\n g: Math.round((gPrime + lightnessOffset) * 255),\n b: Math.round((bPrime + lightnessOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsl(h, s%, l%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsl(rgba: Rgba): HslString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const lightness = (max + min) / 2;\n\n if (max === min) {\n return `hsl(0, 0%, ${Math.round(lightness * 100)}%)` as HslString;\n }\n\n const delta = max - min;\n const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);\n let hue: number;\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n\n return `hsl(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(lightness * 100)}%)` as HslString;\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { requireFinite } from '../math/clamp';
2
- import { hueSectorToPrime } from '../math/hueSector';
1
+ import { requireFinite } from '../math/clamp.js';
2
+ import { hueSectorToPrime } from '../math/hueSector.js';
3
3
  const HSL_RE = /^hsl\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)%?\s*,\s*(-?\d+(?:\.\d+)?)%?\s*\)$/i;
4
4
  /**
5
5
  * Parse HSL input (string or object) into canonical Rgba.
@@ -1 +1 @@
1
- {"version":3,"file":"hsl.js","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hsl.js","sourceRoot":"","sources":["../../src/conversions/hsl.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireFinite } from '../math/clamp.js';\nimport { hueSectorToPrime } from '../math/hueSector.js';\nimport type { HslInput, HslString, Rgba } from '../types.js';\n\nconst HSL_RE =\n /^hsl\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*,\\s*(-?\\d+(?:\\.\\d+)?)%?\\s*\\)$/i;\n\n/**\n * Parse HSL input (string or object) into canonical Rgba.\n * Hue wraps modulo 360; saturation and lightness are 0..100 percent.\n */\nexport function hslToRgba(input: HslInput): Rgba {\n let h: number;\n let s: number;\n let l: number;\n\n if (typeof input === 'string') {\n const match = HSL_RE.exec(input);\n if (!match) throw new Error(`Invalid hsl string: ${input}`);\n h = Number(match[1]);\n s = Number(match[2]);\n l = Number(match[3]);\n } else {\n h = requireFinite(input.h, 'h');\n s = requireFinite(input.s, 's');\n l = requireFinite(input.l, 'l');\n }\n\n const saturation = s / 100;\n const lightness = l / 100;\n\n if (saturation === 0) {\n const gray = Math.round(lightness * 255);\n return { r: gray, g: gray, b: gray, a: 1 };\n }\n\n const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;\n const hueSector = (((h % 360) + 360) % 360) / 60;\n const secondary = chroma * (1 - Math.abs((hueSector % 2) - 1));\n const lightnessOffset = lightness - chroma / 2;\n\n const [rPrime, gPrime, bPrime] = hueSectorToPrime(hueSector, chroma, secondary);\n return {\n r: Math.round((rPrime + lightnessOffset) * 255),\n g: Math.round((gPrime + lightnessOffset) * 255),\n b: Math.round((bPrime + lightnessOffset) * 255),\n a: 1,\n };\n}\n\n/**\n * Emit an `hsl(h, s%, l%)` string from Rgba. Alpha is dropped; all\n * three components are rounded to whole numbers.\n */\nexport function rgbaToHsl(rgba: Rgba): HslString {\n const rNorm = rgba.r / 255;\n const gNorm = rgba.g / 255;\n const bNorm = rgba.b / 255;\n const max = Math.max(rNorm, gNorm, bNorm);\n const min = Math.min(rNorm, gNorm, bNorm);\n const lightness = (max + min) / 2;\n\n if (max === min) {\n return `hsl(0, 0%, ${Math.round(lightness * 100)}%)` as HslString;\n }\n\n const delta = max - min;\n const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);\n let hue: number;\n if (max === rNorm) {\n hue = (gNorm - bNorm) / delta + (gNorm < bNorm ? 6 : 0);\n } else if (max === gNorm) {\n hue = (bNorm - rNorm) / delta + 2;\n } else {\n hue = (rNorm - gNorm) / delta + 4;\n }\n hue *= 60;\n\n return `hsl(${Math.round(hue)}, ${Math.round(saturation * 100)}%, ${Math.round(lightness * 100)}%)` as HslString;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { HsvInput, HsvString, Rgba } from '../types';
1
+ import type { HsvInput, HsvString, Rgba } from '../types.js';
2
2
  /**
3
3
  * Parse HSV input (string or object) into canonical Rgba.
4
4
  * Hue wraps modulo 360; saturation and value are 0..100 percent.