chromonym 3.1.0 → 3.2.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 (81) hide show
  1. package/NOTICE.md +243 -1
  2. package/README.md +49 -44
  3. package/dist/convert.d.ts +73 -21
  4. package/dist/convert.d.ts.map +1 -1
  5. package/dist/convert.js +13 -2
  6. package/dist/convert.js.map +1 -1
  7. package/dist/defineColorPalette.d.ts +53 -0
  8. package/dist/defineColorPalette.d.ts.map +1 -0
  9. package/dist/defineColorPalette.js +103 -0
  10. package/dist/defineColorPalette.js.map +1 -0
  11. package/dist/detectFormat.d.ts +26 -3
  12. package/dist/detectFormat.d.ts.map +1 -1
  13. package/dist/detectFormat.js +20 -0
  14. package/dist/detectFormat.js.map +1 -1
  15. package/dist/identify.d.ts +63 -17
  16. package/dist/identify.d.ts.map +1 -1
  17. package/dist/identify.js.map +1 -1
  18. package/dist/index.d.ts +13 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +12 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/indexing.d.ts +13 -3
  23. package/dist/indexing.d.ts.map +1 -1
  24. package/dist/indexing.js +67 -19
  25. package/dist/indexing.js.map +1 -1
  26. package/dist/palettes/crayola.d.ts +16 -0
  27. package/dist/palettes/crayola.d.ts.map +1 -1
  28. package/dist/palettes/crayola.js +16 -0
  29. package/dist/palettes/crayola.js.map +1 -1
  30. package/dist/palettes/fs595b.d.ts +467 -0
  31. package/dist/palettes/fs595b.d.ts.map +1 -0
  32. package/dist/palettes/fs595b.js +256 -0
  33. package/dist/palettes/fs595b.js.map +1 -0
  34. package/dist/palettes/fs595c.d.ts +1224 -0
  35. package/dist/palettes/fs595c.d.ts.map +1 -0
  36. package/dist/palettes/fs595c.js +633 -0
  37. package/dist/palettes/fs595c.js.map +1 -0
  38. package/dist/palettes/isccNbs.d.ts +562 -0
  39. package/dist/palettes/isccNbs.d.ts.map +1 -0
  40. package/dist/palettes/isccNbs.js +300 -0
  41. package/dist/palettes/isccNbs.js.map +1 -0
  42. package/dist/palettes/nbs.d.ts +578 -0
  43. package/dist/palettes/nbs.d.ts.map +1 -0
  44. package/dist/palettes/nbs.js +309 -0
  45. package/dist/palettes/nbs.js.map +1 -0
  46. package/dist/palettes/ncs.d.ts +3949 -0
  47. package/dist/palettes/ncs.d.ts.map +1 -0
  48. package/dist/palettes/ncs.js +2011 -0
  49. package/dist/palettes/ncs.js.map +1 -0
  50. package/dist/palettes/ntc.d.ts +3170 -0
  51. package/dist/palettes/ntc.d.ts.map +1 -0
  52. package/dist/palettes/ntc.js +1602 -0
  53. package/dist/palettes/ntc.js.map +1 -0
  54. package/dist/palettes/pantone.d.ts +18 -1
  55. package/dist/palettes/pantone.d.ts.map +1 -1
  56. package/dist/palettes/pantone.js +18 -1
  57. package/dist/palettes/pantone.js.map +1 -1
  58. package/dist/palettes/resene.d.ts +2802 -0
  59. package/dist/palettes/resene.d.ts.map +1 -0
  60. package/dist/palettes/resene.js +1422 -0
  61. package/dist/palettes/resene.js.map +1 -0
  62. package/dist/palettes/web.d.ts +10 -0
  63. package/dist/palettes/web.d.ts.map +1 -1
  64. package/dist/palettes/web.js +10 -0
  65. package/dist/palettes/web.js.map +1 -1
  66. package/dist/palettes/x11.d.ts +15 -10
  67. package/dist/palettes/x11.d.ts.map +1 -1
  68. package/dist/palettes/x11.js +15 -10
  69. package/dist/palettes/x11.js.map +1 -1
  70. package/dist/palettes/xkcd.d.ts +1880 -0
  71. package/dist/palettes/xkcd.d.ts.map +1 -0
  72. package/dist/palettes/xkcd.js +955 -0
  73. package/dist/palettes/xkcd.js.map +1 -0
  74. package/dist/resolve.d.ts +58 -14
  75. package/dist/resolve.d.ts.map +1 -1
  76. package/dist/resolve.js +32 -16
  77. package/dist/resolve.js.map +1 -1
  78. package/dist/types.d.ts +47 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/types.js.map +1 -1
  81. package/package.json +45 -4
package/NOTICE.md CHANGED
@@ -67,7 +67,7 @@ Crayola's published approximations.
67
67
 
68
68
  ### Scope
69
69
 
70
- The shipped set is ~85 entries — the current 64-standard box, the
70
+ The shipped set is 84 entries: the current 64-standard box, the
71
71
  iconic fluorescent "Neon" range, and widely-recognized modern
72
72
  additions. It is not exhaustive: historical, retired, specialty-box
73
73
  (Gem Tones, Heads 'N Tails, Multicultural, etc.), and regional-variant
@@ -93,6 +93,248 @@ the public domain.
93
93
  The `web` palette mirrors the CSS Color Module Level 4 named-color
94
94
  set, which is a public specification.
95
95
 
96
+ ## Resene
97
+
98
+ The `resene` palette (1378 entries) is derived from the Raku
99
+ module **`Color::Names::Resene`** in the
100
+ [`thundergnat/Color-Names`](https://github.com/thundergnat/Color-Names)
101
+ repository, which digitizes the named-color catalog published by
102
+ **Resene Paints Ltd.** (New Zealand). Resene's naming system has
103
+ distinctly local flavor, including te reo Māori and New Zealand
104
+ place names ("Pohutukawa," "Acapulco," "Afghan Tan").
105
+
106
+ - Chromonym uses **nominative references** to Resene's published
107
+ color names; no trademark claim or endorsement is implied.
108
+ - Canonical keys are the lowercase single-word form the source
109
+ module uses (`treepoppy` for "Tree Poppy"); the display form
110
+ and Resene's own Munsell-style code (e.g. `BR32-011-076`) are
111
+ captured as trailing comments on each palette entry for human
112
+ readers.
113
+ - sRGB hex values are taken verbatim from the upstream module's
114
+ RGB byte triplets. Resene's physical paint samples under
115
+ their own published lighting conditions are the authoritative
116
+ reference; this digital approximation is provided for
117
+ comparative / tooling use, not for paint specification.
118
+ - If Resene Paints Ltd. objects to this palette's inclusion,
119
+ open an issue and we will revisit.
120
+
121
+ ## Natural Colour System (NCS)
122
+
123
+ The `ncs` palette (1950 entries) is derived from the Raku module
124
+ **`Color::Names::NCS`** in the
125
+ [`thundergnat/Color-Names`](https://github.com/thundergnat/Color-Names)
126
+ repository, which digitizes the **Natural Colour System®** Atlas.
127
+ NCS is a Swedish perceptual color system based on Hering's
128
+ opponent-process theory; its notation (e.g. `S 2030-R80B`)
129
+ describes each color by its blackness, chromaticness, and hue
130
+ position between the four elementary chromatics.
131
+
132
+ - **NCS® is a registered trademark of NCS Colour AB** (Stockholm).
133
+ Chromonym is **not affiliated with, endorsed by, licensed by,
134
+ or certified by NCS Colour AB**. The shipped palette is a
135
+ community approximation intended for on-screen reference and
136
+ nearest-name identification; it is **not suitable for paint
137
+ specification or color matching** against the official NCS
138
+ Atlas.
139
+ - NCS is **device-independent by design**. Any sRGB approximation
140
+ is lossy by construction. The values shipped here come from
141
+ thundergnat's Raku module's own digitization; different NCS
142
+ publications give slightly different sRGB values for the same
143
+ code.
144
+ - Canonical keys are the NCS codes themselves (e.g. `2030-R80B`).
145
+ The palette's custom normalizer accepts optional `NCS ` and
146
+ `S ` prefixes, so `NCS S 2030-R80B`, `S 2030-R80B`,
147
+ `2030-R80B`, and `2030r80b` all resolve identically. `S` is
148
+ only treated as a prefix when followed by a digit, so inputs
149
+ that don't match an NCS code aren't silently rewritten.
150
+
151
+ ### Accuracy envelope
152
+
153
+ NCS specifies colors by percentages on an opponent-process model;
154
+ those percentages must be translated to sRGB through a calibration
155
+ that differs between publications. As a rough guide for the
156
+ digitization chromonym ships:
157
+
158
+ - **Mid-chroma / mid-value codes** (blackness and chromaticness
159
+ both between ~20 and ~60) are the best-behaved; the upstream
160
+ digitization is usually within a few ΔE2000 of any reasonable
161
+ alternate rendering.
162
+ - **High chromaticness, low blackness** codes (e.g. `S 1080-Y90R`,
163
+ `S 0580-G`) sit at or outside the sRGB gamut boundary. The hex
164
+ shipped here is gamut-clipped; a different digitization may
165
+ clip differently, producing visibly different hex values for
166
+ the same NCS code.
167
+ - **Very low-blackness neutrals** (e.g. the `0300-N` / `0500-N`
168
+ range) are close to sRGB white; fine-grained differences between
169
+ digitizations are largely imperceptible.
170
+
171
+ If brand-critical or specification-critical NCS matching is needed,
172
+ consult NCS Colour AB directly: <https://ncscolour.com/>.
173
+
174
+ ## NBS (thundergnat digitization)
175
+
176
+ The `nbs` palette (267 entries) is a **second digitization of the
177
+ same 1955 NBS Method of Designating Colors** covered by the
178
+ `isccNbs` palette above; it is sourced from the Raku module
179
+ **`Color::Names::NBS`** in
180
+ [`thundergnat/Color-Names`](https://github.com/thundergnat/Color-Names).
181
+ Ships alongside `isccNbs` (not as a replacement) because the two
182
+ digitizations produce materially different sRGB values for the
183
+ same named blocks: average ΔE2000 of ~4.6 across the 260 shared
184
+ names, max ΔE2000 of 31 for the most saturated blocks like
185
+ "Vivid bluish green."
186
+
187
+ The two palettes serve different reference workflows:
188
+
189
+ - **`isccNbs`** (Paul Centore) computes Munsell-centroid CIELAB
190
+ then maps to sRGB. "Vivid" blocks land near the sRGB saturation
191
+ edge; seven blocks are left as gaps because their centroids
192
+ fall outside the sRGB gamut.
193
+ - **`nbs`** (thundergnat) appears to use the sample chip RGBs
194
+ from the original NBS publication, which were already gamut-
195
+ compressed by the physical chip material. "Vivid" blocks read
196
+ as more muted, and all 267 entries are covered — including
197
+ the seven blocks whose Munsell centroids fall outside the sRGB
198
+ gamut and are therefore omitted from `isccNbs`. `nbs` does not
199
+ "recover" the out-of-gamut centroids; it ships the already-
200
+ compressed physical chip approximation instead, which is a
201
+ different kind of lossy.
202
+
203
+ Pick `nbs` for historical / swatch-book matching; pick `isccNbs`
204
+ for modern digital design work. The name vocabulary (the 1955
205
+ NBS publication itself) is a US-government document not subject
206
+ to copyright restrictions when faithfully reproduced; the
207
+ specific sRGB values in each palette are credited to their
208
+ respective digitizers.
209
+
210
+ ## ISCC-NBS Colour System
211
+
212
+ The `isccNbs` palette (260 entries) is the **ISCC-NBS Method of
213
+ Designating Colors**, a 1955 joint publication of the
214
+ **Inter-Society Color Council** and the US **National Bureau of
215
+ Standards** (now NIST). The system partitions the Munsell color
216
+ solid into 267 named blocks at Level 3 resolution with canonical
217
+ names like "Vivid pink," "Strong purplish red," and "Dark
218
+ yellowish brown."
219
+
220
+ sRGB centroids for each named block are scraped from **Paul
221
+ Centore's** digitization at
222
+ <https://www.munsellcolorscienceforpainters.com/ISCCNBS/ISCCNBSSystem.html>,
223
+ part of his *Munsell Colour Science for Painters* site.
224
+
225
+ **Why 260 and not 267:** Seven ISCC-NBS blocks sit outside the
226
+ sRGB gamut (notably the most-saturated greens, blues, and
227
+ orange-yellows): `Deep green`, `Deep bluish green`, `Vivid
228
+ greenish blue`, `Deep greenish blue`, `Brilliant orange`, `Deep
229
+ olive green`, and `Vivid orange yellow`. Centore's source marks
230
+ these as `rgb(NaN,NaN,NaN)`; chromonym skips them rather than
231
+ ship a fabricated sRGB approximation.
232
+
233
+ The ISCC-NBS name system itself is a US-government publication
234
+ and is not subject to copyright restrictions when faithfully
235
+ reproduced. Paul Centore's specific sRGB-centroid computations
236
+ are credited here; if the author objects to Chromonym's use of
237
+ his digitization, open an issue and we will revisit or redo the
238
+ centroids from the primary Munsell data.
239
+
240
+ ## Federal Standard 595 (FS595B and FS595C)
241
+
242
+ The `fs595b` (209 entries) and `fs595c` (589 entries) palettes
243
+ are derived from the Raku modules **`Color::Names::FS595B`** and
244
+ **`Color::Names::FS595C`** in the
245
+ [`thundergnat/Color-Names`](https://github.com/thundergnat/Color-Names)
246
+ repository, which digitize the US Federal Government's
247
+ **Federal Standard 595** paint-color specification (used for
248
+ military, aviation, and government-equipment coatings). FS595B
249
+ is the 1989 revision, superseded by FS595C in 2008. Both are
250
+ still in active use depending on which revision a given piece of
251
+ documentation or equipment references. The Federal Standard
252
+ itself is a US-government-published specification and is not
253
+ subject to copyright restrictions when faithfully reproduced.
254
+
255
+ - Canonical keys in both palettes are the 5-digit FS codes
256
+ prefixed with `FS ` (e.g. `FS 11136`). Common names attached
257
+ to many chips in the source (e.g. "Insignia Red") are captured
258
+ as trailing code comments on each palette entry for human
259
+ readers but are **not** used as lookup keys because the spec
260
+ reuses many across multiple chips with different hex values
261
+ (18–20 different "Green"s, 11 "Tan"s, etc., depending on
262
+ revision).
263
+ - Same-coded chips can differ in hex between the B and C
264
+ revisions; for example `FS 11136` ("Insignia Red") is
265
+ `#9b2f25` in FS595B versus `#a32b25` in FS595C. Pick the
266
+ palette that matches your reference documentation.
267
+ - sRGB hex values are taken verbatim from the upstream modules'
268
+ RGB byte triplets; the published standard specifies color
269
+ chips under controlled lighting, and all digital sRGB
270
+ approximations including these are necessarily simplified.
271
+ - Chromonym is **not affiliated with** any US Government agency
272
+ or defense contractor; no endorsement is implied.
273
+
274
+ ## XKCD color survey
275
+
276
+ The `xkcd` palette (923 entries after normalization-collision
277
+ dedupe) is derived from **Randall Munroe's 2010 XKCD color
278
+ survey results**, published at <https://xkcd.com/color/rgb.txt>
279
+ and released to the **public domain under CC0 1.0 Universal**.
280
+ Chromonym reproduces the dataset verbatim apart from:
281
+
282
+ - Hex casing normalized to lowercase.
283
+ - Entries whose names collapse to the same canonical key under
284
+ `standardNormalize` are deduped first-seen wins (e.g. the
285
+ survey's `darkblue` and `dark blue` entries both normalize to
286
+ `darkblue`; only the first is retained).
287
+
288
+ The XKCD color-survey names are crowd-sourced and include
289
+ colorful non-standard descriptors. Chromonym takes no editorial
290
+ stance on the names and ships them as-is.
291
+
292
+ ## Name That Color (NTC)
293
+
294
+ The `ntc` palette (1566 entries) is derived from Chirag Mehta's
295
+ **"Name That Color"** dataset, first published at
296
+ <https://chir.ag/projects/ntc/>. Chromonym consumes the dataset via
297
+ the MIT-licensed **`colorjs/color-namer`** npm package
298
+ (<https://github.com/colorjs/color-namer>), which redistributes the
299
+ list in a machine-readable form. The shipped palette was generated
300
+ against commit `abb3d184ab63db9327908319cc45b55c91493bb7` of the
301
+ upstream repository, so the entry set is deterministic and
302
+ reproducible.
303
+
304
+ - Names are used as **nominative references** to the publicly
305
+ distributed NTC dataset; no endorsement or trademark claim is
306
+ implied.
307
+ - sRGB hex values are taken verbatim from the upstream list.
308
+ - No modifications are made beyond casing normalization of the hex
309
+ string and sorting by display name.
310
+
311
+ The NTC dataset has been widely redistributed since 2006 under
312
+ terms allowing free reuse with attribution. If the upstream author
313
+ objects to Chromonym's inclusion of the list, open an issue and we
314
+ will revisit.
315
+
316
+ ## Bauhaus Modern (demo site typeface)
317
+
318
+ The demo site at <https://simiancraft.github.io/chromonym/> embeds
319
+ **Bauhaus Modern**, a display typeface designed by **Nils Kähler**
320
+ (2016). The font file is hosted under
321
+ [`demo/public/fonts/`](./demo/public/fonts/) and is used only as
322
+ display type for the demo wordmark.
323
+
324
+ - Source: <https://www.dafont.com/bauhaus-modern.font>
325
+ - License terms: "Free for personal use" (per the dafont listing)
326
+ - Use for this demo was arranged with the designer's knowledge via
327
+ email; a donation was made, and the use case (MIT-licensed library
328
+ demo, display-type only, with visible attribution) was described.
329
+ See [`demo/public/fonts/README.txt`](./demo/public/fonts/README.txt)
330
+ for the full note.
331
+
332
+ The font is not part of the published npm package (`files` allowlist
333
+ ships only the library, not the demo site). Downstream users of the
334
+ `chromonym` package do not redistribute Bauhaus Modern by installing
335
+ the library. Please do not repackage or redistribute the font file
336
+ outside this demo without following up with the designer directly.
337
+
96
338
  ## Upstream dependencies
97
339
 
98
340
  See `package.json` for runtime and development dependencies. All are MIT
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src=".github/assets/banner.png" alt="chromonym" width="800" />
2
+ <img src=".github/assets/chromonym_transparent_tight.webp" alt="chromonym" width="800" />
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -20,11 +20,15 @@
20
20
  <code>identify</code> &nbsp;•&nbsp; <code>resolve</code> &nbsp;•&nbsp; <code>convert</code>
21
21
  </p>
22
22
 
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, Pantone, and Crayola out of the box; bring your own (brand colors, paint chips, game factions, chart themes) with full TypeScript inference and zero registration. Six distance metrics — from fast Euclidean to industry-standard CIEDE2000 and OKLAB. Tree-shakes to the bone — you only pay for palettes you import.
23
+ **The last color-naming library you'll ever need.** One API — `identify`, `resolve`, `convert` — works against any palette you throw at it. Ships twelve palettes out of the box (CSS, X11, Pantone, Crayola, NTC, XKCD, Resene, NCS, ISCC-NBS, NBS, FS 595 B, FS 595 C); bring your own (brand colors, paint chips, game factions, chart themes) with full TypeScript inference and zero registration. Six distance metrics — from fast Euclidean to industry-standard CIEDE2000 and OKLAB. Tree-shakes to the bone — you only pay for palettes you import.
24
+
25
+ <sub>*chromonym* (n.): a name for a color; *chroma* + *-onym*. Also the library.</sub>
26
+
27
+ <sub>Every public export ships with JSDoc and a runnable `@example`; autocomplete and hover do the teaching.</sub>
24
28
 
25
29
  <p align="center">
26
30
  <a href="https://simiancraft.github.io/chromonym/">
27
- <img src=".github/assets/chromonym-demo.gif" alt="chromonym demo — scrub a color, see the nearest name update live" width="800" />
31
+ <img src=".github/assets/chromonym-demo-simiancraft.gif" alt="chromonym demo — scrub a color, see the nearest name update live" width="800" />
28
32
  </a>
29
33
  </p>
30
34
 
@@ -80,18 +84,6 @@ yarn add chromonym
80
84
  npm install chromonym
81
85
  ```
82
86
 
83
- ## Migrating from 1.x
84
-
85
- v2 collapsed the five-verb draft API (`identify`, `identifyAll`, `translate`, `resolve`, `convert`) into **three verbs** by folding the duplicates into options on existing verbs. Mechanical rewrites:
86
-
87
- ```ts
88
- // v1 → v2
89
- identifyAll(color, { palette, k: 3 }) // → identify(color, { palette, k: 3 })
90
- translate(name, { from: web, to: pantone }) // → identify(name, { palette: pantone, source: web })
91
- ```
92
-
93
- `resolve` and `convert` are unchanged. If you were only using those two, no edits are needed. See `CHANGELOG.md` for the full list of breaking changes.
94
-
95
87
  ## API
96
88
 
97
89
  Three verbs. All accept an optional `opts` object; defaults fire when omitted.
@@ -273,22 +265,24 @@ type Palette<Name extends string = string> = {
273
265
 
274
266
  The primary shape. Any object matching `Palette<Name>` works — no registry, no plugin, no side effects. Bundlers only include the palettes you actually import.
275
267
 
268
+ The recommended pattern is `defineColorPalette`: it accepts mixed color-value formats (paste whatever came out of your design tool), normalizes everything to hex once at module load, and preserves the literal-key union for full narrowing through `identify` / `resolve` / `convert`.
269
+
276
270
  ```ts
277
- import { identify, resolve, type Palette } from 'chromonym';
271
+ import { defineColorPalette, identify, resolve } from 'chromonym';
278
272
 
279
- const warhammer = {
273
+ const warhammer = defineColorPalette({
280
274
  name: 'warhammer40k',
281
275
  colors: {
282
- 'world eaters red': '#8b1a1a',
283
- 'adeptus red': '#652022',
284
- 'sons of malice white': '#e8e4d8',
285
- 'the flawless host purple': '#6b2d7d',
286
- 'nurgle green': '#748c3f',
276
+ 'world eaters red': '#8b1a1a', // hex
277
+ 'adeptus red': 'rgb(101, 32, 34)', // rgb string
278
+ 'sons of malice white': { r: 232, g: 228, b: 216 }, // rgb object
279
+ 'the flawless host purple': [107, 45, 125], // rgb tuple
280
+ 'nurgle green': 'hsl(78, 38%, 39%)', // hsl string
287
281
  'alpha legion teal': '#2a6d7a',
288
282
  },
289
- normalize: (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
283
+ normalize: (s) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
290
284
  defaultMetric: 'deltaE2000',
291
- } as const satisfies Palette;
285
+ });
292
286
 
293
287
  identify('#750c0c', { palette: warhammer })
294
288
  // → 'world eaters red' (return type narrows to the palette's key union)
@@ -297,14 +291,26 @@ resolve('Nurgle Green', { palette: warhammer })
297
291
  // → '#748c3f' (your normalizer handles case + punctuation)
298
292
  ```
299
293
 
300
- ### The four we ship
294
+ Unparseable values are dropped with a `console.warn` naming the palette, the offending key, and the bad value; the rest of the palette loads normally. This keeps a single typo out of your brand kit from taking down the whole module.
295
+
296
+ If you prefer zero runtime and already have hex-only values, the `as const satisfies Palette<'A' | 'B' | ...>` pattern still works — list the key union once in the generic and it narrows identically.
297
+
298
+ ### The twelve we ship
301
299
 
302
300
  | Name | Entries | Source |
303
301
  |---|---|---|
304
302
  | `web` (default) | 148 | CSS Color Module Level 4 named colors |
305
303
  | `x11` | 658 | X.Org `rgb.txt` (public domain) |
306
304
  | `pantone` | 907 | Pantone Coated (C) — community approximations (not Pantone-licensed) |
307
- | `crayola` | ~85 | Crayola crayon colors — community approximations (not Crayola-licensed) |
305
+ | `crayola` | 84 | Crayola crayon colors — community approximations (not Crayola-licensed) |
306
+ | `ntc` | 1566 | Chirag Mehta's "Name That Color" dataset (broad coverage of design / UI / hardware names) |
307
+ | `xkcd` | 923 | XKCD 2010 color-survey results (CC0; crowd-sourced common-speech names) |
308
+ | `fs595b` | 209 | Federal Standard 595B paint chips (1989 revision; modelers / historical equipment) |
309
+ | `fs595c` | 589 | Federal Standard 595C paint chips (US Government spec; military / aviation coatings) |
310
+ | `isccNbs` | 260 | ISCC-NBS Method of Designating Colors (1955 US standard; Munsell-partitioned named blocks) |
311
+ | `nbs` | 267 | Second digitization of the same 1955 standard; different sRGB (matches 1955 chip book; covers the 7 blocks `isccNbs` leaves as gaps) |
312
+ | `resene` | 1378 | Resene Paints (NZ) catalog; te reo Māori and NZ place-names in the naming |
313
+ | `ncs` | 1950 | Natural Colour System (Sweden) — perceptual, structured notation (`2030-R80B`), not NCS-licensed |
308
314
 
309
315
  Importable directly, or via subpath exports for stricter tree-shaking:
310
316
 
@@ -321,7 +327,7 @@ crayola.colors['Granny Smith Apple'] // '#a8e4a0'
321
327
 
322
328
  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'`), `crayola` uses the Title Case form printed on the crayon wrapper (`'Granny Smith Apple'`).
323
329
 
324
- *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.
330
+ *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 100, so `x11.colors['gray 73']` is a real key (`#bababa`). Yes, these are Greys. No, not the kind that visit during sleep paralysis.
325
331
 
326
332
  ### Cross-palette translation
327
333
 
@@ -382,7 +388,7 @@ Rule of thumb: use `source` when you want one clean call and `null` on miss; use
382
388
 
383
389
  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.
384
390
 
385
- > **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.
391
+ > **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' }` for better separation in the blue and purple region.
386
392
 
387
393
  ```ts
388
394
  // Defaults: read from the palette's own `defaultMetric`.
@@ -443,6 +449,20 @@ const tryConvert = (input: ColorInput, opts = {}) => {
443
449
  - BYO palettes have zero library cost beyond your own data.
444
450
  - 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.
445
451
 
452
+ ### What you actually ship
453
+
454
+ The published tarball is **~557 kB gzipped / ~2.5 MB unpacked** because it carries twelve palettes and ~9000 named-color entries. That's the number `packagephobia` reports — it measures the install footprint on disk. Your **bundle** pays only for what you actually import, measured with `esbuild --minify`:
455
+
456
+ | Import | min | gzip |
457
+ |---|---|---|
458
+ | `import { identify, web } from 'chromonym'` | 10.9 kB | **4.7 kB** |
459
+ | `import { identify, pantone } from 'chromonym'` | 27 kB | **11.3 kB** |
460
+ | `import { identify, ntc } from 'chromonym'` (one of the bigger palettes) | 43 kB | **20 kB** |
461
+ | `import { defineColorPalette } from 'chromonym'` (pure BYO; no built-in) | 3.8 kB | **1.5 kB** |
462
+ | `import { pantoneToRgba } from 'chromonym'` (single conversion) | 17.5 kB | **6.8 kB** |
463
+
464
+ The install-size number is a shipping concern, not a bundle-size one. If you're evaluating chromonym against a size budget, the gzip column above is what actually lands in your production build.
465
+
446
466
  ## Types
447
467
 
448
468
  Re-exported from the root barrel — `import type { ... } from 'chromonym'`:
@@ -466,17 +486,6 @@ bun test
466
486
  bun run build
467
487
  ```
468
488
 
469
- Regenerating palette data:
470
-
471
- ```sh
472
- bun run scripts/generate-x11.ts # reads /usr/share/X11/rgb.txt
473
- bun run scripts/generate-pantone.ts # reads the color_library npm package (MIT © Radu Dragan)
474
- ```
475
-
476
- ## Coverage
477
-
478
- [![Coverage sunburst](https://codecov.io/github/simiancraft/chromonym/graphs/sunburst.svg?token=HYWM6G66YE)](https://codecov.io/github/simiancraft/chromonym)
479
-
480
489
  ## See also
481
490
 
482
491
  chromonym deliberately limits its scope to color *naming* (resolving and identifying) and lightweight conversions. For other color-related work, reach for tools that do those things well:
@@ -491,8 +500,4 @@ MIT © [the-simian](https://github.com/the-simian)
491
500
 
492
501
  <p align="center"><sub>Crafted with care by <a href="https://simiancraft.com">Simiancraft</a>.</sub></p>
493
502
 
494
- ---
495
-
496
- > ### Pantone® trademark notice
497
- >
498
- > **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.
503
+ <p align="center"><sub>Pantone® and Crayola® are trademarks of their respective owners; Chromonym is not affiliated. Palette values are community approximations. Demo typeface <a href="https://www.dafont.com/bauhaus-modern.font">Bauhaus Modern</a> © Nils Kähler. Full attributions: <a href="./NOTICE.md">NOTICE.md</a>.</sub></p>
package/dist/convert.d.ts CHANGED
@@ -1,29 +1,34 @@
1
1
  import { type DetectedFormat } from './detectFormat.js';
2
- import type { ColorFormat, ColorInput, ColorValue, Palette, Rgba } from './types.js';
2
+ import type { ColorFormat, ColorInput, ColorValue, Palette, PaletteKey, Rgba } from './types.js';
3
3
  /**
4
4
  * Normalize any recognized color input to canonical `Rgba`.
5
5
  * Pass `knownFormat` when the caller has already run `detectFormat` to skip
6
6
  * the redundant detection. Throws on `UNKNOWN` input.
7
7
  *
8
- * Palette names (Pantone codes, BYO keys) are not accepted here — they
8
+ * Palette names (Pantone codes, BYO keys) are not accepted here; they
9
9
  * require palette data to parse. Use `convert(name, { palette })` or
10
10
  * `resolve(name, { palette })` at the higher level.
11
+ *
12
+ * @example
13
+ * toRgba('#ff0000'); // { r: 255, g: 0, b: 0, a: 1 }
14
+ * toRgba('rgb(255, 0, 0)'); // { r: 255, g: 0, b: 0, a: 1 }
15
+ * toRgba([255, 0, 0]); // { r: 255, g: 0, b: 0, a: 1 }
16
+ * toRgba({ h: 0, s: 100, l: 50 }); // { r: 255, g: 0, b: 0, a: 1 }
11
17
  */
12
18
  export declare function toRgba(input: ColorInput, knownFormat?: DetectedFormat): Rgba;
13
19
  /**
14
20
  * Emit a canonical `Rgba` in the target structural format. Palette-independent.
15
21
  *
16
22
  * For palette-key output (e.g. nearest Pantone code, brand name), use
17
- * `convert(rgba, { format: 'NAME', palette })` — or `identify` for fuzzy,
23
+ * `convert(rgba, { format: 'NAME', palette })`; or `identify` for fuzzy,
18
24
  * `rgbaToPantone` for the low-level Pantone-only path.
25
+ *
26
+ * @example
27
+ * fromRgba({ r: 255, g: 0, b: 0, a: 1 }); // '#ff0000'
28
+ * fromRgba({ r: 255, g: 0, b: 0, a: 1 }, 'HSL'); // 'hsl(0, 100%, 50%)'
29
+ * fromRgba({ r: 255, g: 0, b: 0, a: 0.5 }, 'RGB'); // 'rgb(255, 0, 0)' (alpha dropped)
19
30
  */
20
31
  export declare function fromRgba(rgba: Rgba, format?: ColorFormat): ColorValue;
21
- /**
22
- * Extract string keys from a Palette's `colors` map.
23
- * Matches the `PaletteKey<P>` helper in `identify.ts` — kept local so
24
- * `convert` doesn't depend on `identify`.
25
- */
26
- type PaletteKey<P extends Palette> = Extract<keyof P['colors'], string>;
27
32
  /**
28
33
  * Detect the input color format, normalize to Rgba, and emit the result
29
34
  * in the requested output format.
@@ -46,16 +51,63 @@ type PaletteKey<P extends Palette> = Extract<keyof P['colors'], string>;
46
51
  * Throws on unrecognized input (parser-flavored — `identify`/`resolve`
47
52
  * are the `null`-returning alternatives).
48
53
  */
49
- export declare function convert(input: ColorInput, opts?: {
50
- format?: ColorFormat;
51
- }): ColorValue;
52
- export declare function convert<P extends Palette>(input: ColorInput | string, opts: {
53
- palette: P;
54
- format: 'NAME';
55
- }): PaletteKey<P>;
56
- export declare function convert<P extends Palette>(input: ColorInput | string, opts: {
57
- palette: P;
58
- format?: ColorFormat;
59
- }): ColorValue;
60
- export {};
54
+ /**
55
+ * Options for the structural (palette-free) variant of {@link convert}.
56
+ * Use when you only need to translate between HEX / RGB / RGBA / HSL / HSV.
57
+ *
58
+ * @example
59
+ * convert('#ff0000', { format: 'HSL' }); // 'hsl(0, 100%, 50%)'
60
+ */
61
+ export type ConvertStructuralOptions = {
62
+ /**
63
+ * Target output format. Defaults to `'HEX'`.
64
+ */
65
+ readonly format?: ColorFormat;
66
+ };
67
+ /**
68
+ * Options for {@link convert} when reverse-looking-up a palette name
69
+ * from an exact color value. Generic so the return narrows to the
70
+ * palette's literal-key union.
71
+ *
72
+ * Throws at runtime if the input isn't a pixel-exact match in the
73
+ * palette; use `identify` for nearest-match semantics.
74
+ *
75
+ * @example
76
+ * convert('#e4002b', { palette: pantone, format: 'NAME' }); // '185 C'
77
+ * convert('#663399', { palette: web, format: 'NAME' }); // 'rebeccapurple'
78
+ */
79
+ export type ConvertNameOptions<P extends Palette> = {
80
+ /**
81
+ * Palette to reverse-look-up against. Required when `format` is `'NAME'`.
82
+ */
83
+ readonly palette: P;
84
+ /**
85
+ * Output discriminator. `'NAME'` returns the palette's canonical key
86
+ * for the input color.
87
+ */
88
+ readonly format: 'NAME';
89
+ };
90
+ /**
91
+ * Options for {@link convert} when a palette is provided but the output
92
+ * is a structural format (HEX / RGB / HSL / HSV). Unlocks palette-name
93
+ * *input*: the `input` may be `'185 C'`, `'Acme Red'`, etc., which is
94
+ * resolved through `palette.normalize` before conversion.
95
+ *
96
+ * @example
97
+ * convert('185 C', { palette: pantone }); // '#e4002b'
98
+ * convert('185 C', { palette: pantone, format: 'HSL' }); // 'hsl(349, 100%, 45%)'
99
+ */
100
+ export type ConvertPaletteOptions<P extends Palette> = {
101
+ /**
102
+ * Palette used to parse name-shaped inputs.
103
+ */
104
+ readonly palette: P;
105
+ /**
106
+ * Target output format. Defaults to `'HEX'`.
107
+ */
108
+ readonly format?: ColorFormat;
109
+ };
110
+ export declare function convert(input: ColorInput, opts?: ConvertStructuralOptions): ColorValue;
111
+ export declare function convert<P extends Palette>(input: ColorInput | string, opts: ConvertNameOptions<P>): PaletteKey<P>;
112
+ export declare function convert<P extends Palette>(input: ColorInput | string, opts: ConvertPaletteOptions<P>): ColorValue;
61
113
  //# sourceMappingURL=convert.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,UAAU,EAIV,OAAO,EACP,IAAI,EAGL,MAAM,YAAY,CAAC;AAYpB;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,cAAc,GAAG,IAAI,CAgB5E;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,GAAE,WAAmB,GAAG,UAAU,CAqB5E;AAED;;;;GAIG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAAG,UAAU,CAAC;AAGxF,wBAAgB,OAAO,CAAC,CAAC,SAAS,OAAO,EACvC,KAAK,EAAE,UAAU,GAAG,MAAM,EAC1B,IAAI,EAAE;IAAE,OAAO,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnC,UAAU,CAAC,CAAC,CAAC,CAAC;AAGjB,wBAAgB,OAAO,CAAC,CAAC,SAAS,OAAO,EACvC,KAAK,EAAE,UAAU,GAAG,MAAM,EAC1B,IAAI,EAAE;IAAE,OAAO,EAAE,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GACzC,UAAU,CAAC","sourcesContent":["import { hexToRgba, rgbaToHex } from './conversions/hex.js';\nimport { hslToRgba, rgbaToHsl } from './conversions/hsl.js';\nimport { hsvToRgba, rgbaToHsv } from './conversions/hsv.js';\nimport { rgbaToRgb, rgbToRgba } from './conversions/rgb.js';\nimport { type DetectedFormat, detectFormat } from './detectFormat.js';\nimport { getNameIndex, getReverseNameIndex } from './indexing.js';\nimport type {\n ColorFormat,\n ColorInput,\n ColorValue,\n HexColor,\n HslInput,\n HsvInput,\n Palette,\n Rgba,\n RgbaInput,\n RgbInput,\n} from './types.js';\n\nfunction safeStringify(x: unknown): string {\n if (x === undefined) return 'undefined';\n try {\n const s = JSON.stringify(x);\n return s === undefined ? String(x) : s;\n } catch {\n return Object.prototype.toString.call(x);\n }\n}\n\n/**\n * Normalize any recognized color input to canonical `Rgba`.\n * Pass `knownFormat` when the caller has already run `detectFormat` to skip\n * the redundant detection. Throws on `UNKNOWN` input.\n *\n * Palette names (Pantone codes, BYO keys) are not accepted here — they\n * require palette data to parse. Use `convert(name, { palette })` or\n * `resolve(name, { palette })` at the higher level.\n */\nexport function toRgba(input: ColorInput, knownFormat?: DetectedFormat): Rgba {\n const format = knownFormat ?? detectFormat(input);\n switch (format) {\n case 'HEX':\n return hexToRgba(input as HexColor);\n case 'RGB':\n return rgbToRgba(input as RgbInput);\n case 'RGBA':\n return rgbToRgba(input as RgbaInput);\n case 'HSL':\n return hslToRgba(input as HslInput);\n case 'HSV':\n return hsvToRgba(input as HsvInput);\n default:\n throw new Error(`Unrecognized color input: ${safeStringify(input)}`);\n }\n}\n\n/**\n * Emit a canonical `Rgba` in the target structural format. Palette-independent.\n *\n * For palette-key output (e.g. nearest Pantone code, brand name), use\n * `convert(rgba, { format: 'NAME', palette })` — or `identify` for fuzzy,\n * `rgbaToPantone` for the low-level Pantone-only path.\n */\nexport function fromRgba(rgba: Rgba, format: ColorFormat = 'HEX'): ColorValue {\n switch (format) {\n case 'HEX':\n return rgbaToHex(rgba);\n case 'RGB':\n return rgbaToRgb(rgba);\n case 'RGBA':\n return rgba;\n case 'HSL':\n return rgbaToHsl(rgba);\n case 'HSV':\n return rgbaToHsv(rgba);\n default: {\n // Runtime safety for JS callers bypassing the ColorFormat union.\n const hint =\n format === ('PANTONE' as unknown)\n ? ` — use convert(rgba, { palette: pantone, format: 'NAME' }) or rgbaToPantone from 'chromonym/conversions/pantone'`\n : '';\n throw new Error(`Unsupported format: ${safeStringify(format)}${hint}`);\n }\n }\n}\n\n/**\n * Extract string keys from a Palette's `colors` map.\n * Matches the `PaletteKey<P>` helper in `identify.ts` — kept local so\n * `convert` doesn't depend on `identify`.\n */\ntype PaletteKey<P extends Palette> = Extract<keyof P['colors'], string>;\n\n/**\n * Detect the input color format, normalize to Rgba, and emit the result\n * in the requested output format.\n *\n * Without a `palette` option, `convert` is strictly structural —\n * HEX ↔ RGB ↔ RGBA ↔ HSL ↔ HSV, no palette data ever touched, tree-shakes\n * to the bone.\n *\n * With a `palette` option, `convert` understands the palette's naming\n * scheme both ways:\n * - Input can be a palette name (`'185 C'`, `'Acme Red'`) — parsed via\n * `palette.normalize` and looked up in `palette.colors`.\n * - Output can be `format: 'NAME'` — the exact canonical key from\n * `palette.colors`, throws if the color isn't a pixel-exact match (use\n * `identify` for nearest-match).\n *\n * The palette data comes from the caller's own import, so tree-shaking\n * is preserved: callers who don't pass `palette` never pull a palette.\n *\n * Throws on unrecognized input (parser-flavored — `identify`/`resolve`\n * are the `null`-returning alternatives).\n */\n\n// Overload 1: no palette — structural only. Input is strictly ColorInput.\nexport function convert(input: ColorInput, opts?: { format?: ColorFormat }): ColorValue;\n\n// Overload 2: palette present, format 'NAME' — returns a palette key.\nexport function convert<P extends Palette>(\n input: ColorInput | string,\n opts: { palette: P; format: 'NAME' },\n): PaletteKey<P>;\n\n// Overload 3: palette present, structural format — returns ColorValue.\nexport function convert<P extends Palette>(\n input: ColorInput | string,\n opts: { palette: P; format?: ColorFormat },\n): ColorValue;\n\n// Implementation\nexport function convert(\n input: ColorInput | string,\n opts: { format?: ColorFormat | 'NAME'; palette?: Palette } = {},\n): ColorValue | string {\n const { format = 'HEX', palette } = opts;\n\n // --- Parse input → Rgba ---\n // Structural detection first; palette lookup only as a fallback. This\n // way `convert('#ff0000', { palette: brand })` always means \"parse hex,\"\n // even if the brand palette happens to have a key that normalizes to\n // \"ff0000\".\n const detected = detectFormat(input as ColorInput);\n let rgba: Rgba;\n if (detected !== 'UNKNOWN') {\n rgba = toRgba(input as ColorInput, detected);\n } else if (palette !== undefined && typeof input === 'string') {\n const canonical = getNameIndex(palette).get(palette.normalize(input));\n if (canonical === undefined) {\n throw new Error(\n `Unrecognized color input: ${safeStringify(input)} (not structural, and not found in palette '${palette.name}')`,\n );\n }\n const hex = (palette.colors as Record<string, HexColor>)[canonical];\n if (hex === undefined) {\n throw new Error(`Unrecognized color input: ${safeStringify(input)}`);\n }\n rgba = hexToRgba(hex);\n } else {\n throw new Error(`Unrecognized color input: ${safeStringify(input)}`);\n }\n\n // --- Emit ---\n if (format === 'NAME') {\n if (palette === undefined) {\n throw new Error(`convert(_, { format: 'NAME' }) requires a 'palette' option`);\n }\n // Palette colors are alpha = 1 by construction; a partially-transparent\n // input isn't \"that named color\" in the strict-convert sense. Reject\n // here rather than silently drop alpha during the reverse lookup.\n if (rgba.a !== 1) {\n throw new Error(\n `format: 'NAME' requires fully-opaque input (a === 1). Got a=${rgba.a}. ` +\n `Strip alpha first or use identify() for fuzzy matching.`,\n );\n }\n const hex = rgbaToHex(rgba).toLowerCase();\n const name = getReverseNameIndex(palette).get(hex);\n if (name === undefined) {\n throw new Error(\n `No exact match for ${hex} in palette '${palette.name}'. Use identify() for nearest-match semantics.`,\n );\n }\n return name;\n }\n return fromRgba(rgba, format);\n}\n"]}
1
+ {"version":3,"file":"convert.d.ts","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,UAAU,EAIV,OAAO,EACP,UAAU,EACV,IAAI,EAGL,MAAM,YAAY,CAAC;AAYpB;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,cAAc,GAAG,IAAI,CAgB5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,GAAE,WAAmB,GAAG,UAAU,CAqB5E;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,OAAO,IAAI;IAClD;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,OAAO,IAAI;IACrD;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,CAAC;AAGF,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,wBAAwB,GAAG,UAAU,CAAC;AAGxF,wBAAgB,OAAO,CAAC,CAAC,SAAS,OAAO,EACvC,KAAK,EAAE,UAAU,GAAG,MAAM,EAC1B,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC1B,UAAU,CAAC,CAAC,CAAC,CAAC;AAGjB,wBAAgB,OAAO,CAAC,CAAC,SAAS,OAAO,EACvC,KAAK,EAAE,UAAU,GAAG,MAAM,EAC1B,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC7B,UAAU,CAAC","sourcesContent":["import { hexToRgba, rgbaToHex } from './conversions/hex.js';\nimport { hslToRgba, rgbaToHsl } from './conversions/hsl.js';\nimport { hsvToRgba, rgbaToHsv } from './conversions/hsv.js';\nimport { rgbaToRgb, rgbToRgba } from './conversions/rgb.js';\nimport { type DetectedFormat, detectFormat } from './detectFormat.js';\nimport { getNameIndex, getReverseNameIndex } from './indexing.js';\nimport type {\n ColorFormat,\n ColorInput,\n ColorValue,\n HexColor,\n HslInput,\n HsvInput,\n Palette,\n PaletteKey,\n Rgba,\n RgbaInput,\n RgbInput,\n} from './types.js';\n\nfunction safeStringify(x: unknown): string {\n if (x === undefined) return 'undefined';\n try {\n const s = JSON.stringify(x);\n return s === undefined ? String(x) : s;\n } catch {\n return Object.prototype.toString.call(x);\n }\n}\n\n/**\n * Normalize any recognized color input to canonical `Rgba`.\n * Pass `knownFormat` when the caller has already run `detectFormat` to skip\n * the redundant detection. Throws on `UNKNOWN` input.\n *\n * Palette names (Pantone codes, BYO keys) are not accepted here; they\n * require palette data to parse. Use `convert(name, { palette })` or\n * `resolve(name, { palette })` at the higher level.\n *\n * @example\n * toRgba('#ff0000'); // { r: 255, g: 0, b: 0, a: 1 }\n * toRgba('rgb(255, 0, 0)'); // { r: 255, g: 0, b: 0, a: 1 }\n * toRgba([255, 0, 0]); // { r: 255, g: 0, b: 0, a: 1 }\n * toRgba({ h: 0, s: 100, l: 50 }); // { r: 255, g: 0, b: 0, a: 1 }\n */\nexport function toRgba(input: ColorInput, knownFormat?: DetectedFormat): Rgba {\n const format = knownFormat ?? detectFormat(input);\n switch (format) {\n case 'HEX':\n return hexToRgba(input as HexColor);\n case 'RGB':\n return rgbToRgba(input as RgbInput);\n case 'RGBA':\n return rgbToRgba(input as RgbaInput);\n case 'HSL':\n return hslToRgba(input as HslInput);\n case 'HSV':\n return hsvToRgba(input as HsvInput);\n default:\n throw new Error(`Unrecognized color input: ${safeStringify(input)}`);\n }\n}\n\n/**\n * Emit a canonical `Rgba` in the target structural format. Palette-independent.\n *\n * For palette-key output (e.g. nearest Pantone code, brand name), use\n * `convert(rgba, { format: 'NAME', palette })`; or `identify` for fuzzy,\n * `rgbaToPantone` for the low-level Pantone-only path.\n *\n * @example\n * fromRgba({ r: 255, g: 0, b: 0, a: 1 }); // '#ff0000'\n * fromRgba({ r: 255, g: 0, b: 0, a: 1 }, 'HSL'); // 'hsl(0, 100%, 50%)'\n * fromRgba({ r: 255, g: 0, b: 0, a: 0.5 }, 'RGB'); // 'rgb(255, 0, 0)' (alpha dropped)\n */\nexport function fromRgba(rgba: Rgba, format: ColorFormat = 'HEX'): ColorValue {\n switch (format) {\n case 'HEX':\n return rgbaToHex(rgba);\n case 'RGB':\n return rgbaToRgb(rgba);\n case 'RGBA':\n return rgba;\n case 'HSL':\n return rgbaToHsl(rgba);\n case 'HSV':\n return rgbaToHsv(rgba);\n default: {\n // Runtime safety for JS callers bypassing the ColorFormat union.\n const hint =\n format === ('PANTONE' as unknown)\n ? ` — use convert(rgba, { palette: pantone, format: 'NAME' }) or rgbaToPantone from 'chromonym/conversions/pantone'`\n : '';\n throw new Error(`Unsupported format: ${safeStringify(format)}${hint}`);\n }\n }\n}\n\n/**\n * Detect the input color format, normalize to Rgba, and emit the result\n * in the requested output format.\n *\n * Without a `palette` option, `convert` is strictly structural —\n * HEX ↔ RGB ↔ RGBA ↔ HSL ↔ HSV, no palette data ever touched, tree-shakes\n * to the bone.\n *\n * With a `palette` option, `convert` understands the palette's naming\n * scheme both ways:\n * - Input can be a palette name (`'185 C'`, `'Acme Red'`) — parsed via\n * `palette.normalize` and looked up in `palette.colors`.\n * - Output can be `format: 'NAME'` — the exact canonical key from\n * `palette.colors`, throws if the color isn't a pixel-exact match (use\n * `identify` for nearest-match).\n *\n * The palette data comes from the caller's own import, so tree-shaking\n * is preserved: callers who don't pass `palette` never pull a palette.\n *\n * Throws on unrecognized input (parser-flavored — `identify`/`resolve`\n * are the `null`-returning alternatives).\n */\n\n/**\n * Options for the structural (palette-free) variant of {@link convert}.\n * Use when you only need to translate between HEX / RGB / RGBA / HSL / HSV.\n *\n * @example\n * convert('#ff0000', { format: 'HSL' }); // 'hsl(0, 100%, 50%)'\n */\nexport type ConvertStructuralOptions = {\n /**\n * Target output format. Defaults to `'HEX'`.\n */\n readonly format?: ColorFormat;\n};\n\n/**\n * Options for {@link convert} when reverse-looking-up a palette name\n * from an exact color value. Generic so the return narrows to the\n * palette's literal-key union.\n *\n * Throws at runtime if the input isn't a pixel-exact match in the\n * palette; use `identify` for nearest-match semantics.\n *\n * @example\n * convert('#e4002b', { palette: pantone, format: 'NAME' }); // '185 C'\n * convert('#663399', { palette: web, format: 'NAME' }); // 'rebeccapurple'\n */\nexport type ConvertNameOptions<P extends Palette> = {\n /**\n * Palette to reverse-look-up against. Required when `format` is `'NAME'`.\n */\n readonly palette: P;\n /**\n * Output discriminator. `'NAME'` returns the palette's canonical key\n * for the input color.\n */\n readonly format: 'NAME';\n};\n\n/**\n * Options for {@link convert} when a palette is provided but the output\n * is a structural format (HEX / RGB / HSL / HSV). Unlocks palette-name\n * *input*: the `input` may be `'185 C'`, `'Acme Red'`, etc., which is\n * resolved through `palette.normalize` before conversion.\n *\n * @example\n * convert('185 C', { palette: pantone }); // '#e4002b'\n * convert('185 C', { palette: pantone, format: 'HSL' }); // 'hsl(349, 100%, 45%)'\n */\nexport type ConvertPaletteOptions<P extends Palette> = {\n /**\n * Palette used to parse name-shaped inputs.\n */\n readonly palette: P;\n /**\n * Target output format. Defaults to `'HEX'`.\n */\n readonly format?: ColorFormat;\n};\n\n// Overload 1: no palette — structural only. Input is strictly ColorInput.\nexport function convert(input: ColorInput, opts?: ConvertStructuralOptions): ColorValue;\n\n// Overload 2: palette present, format 'NAME' — returns a palette key.\nexport function convert<P extends Palette>(\n input: ColorInput | string,\n opts: ConvertNameOptions<P>,\n): PaletteKey<P>;\n\n// Overload 3: palette present, structural format — returns ColorValue.\nexport function convert<P extends Palette>(\n input: ColorInput | string,\n opts: ConvertPaletteOptions<P>,\n): ColorValue;\n\n// Implementation\nexport function convert(\n input: ColorInput | string,\n opts: { format?: ColorFormat | 'NAME'; palette?: Palette } = {},\n): ColorValue | string {\n const { format = 'HEX', palette } = opts;\n\n // --- Parse input → Rgba ---\n // Structural detection first; palette lookup only as a fallback. This\n // way `convert('#ff0000', { palette: brand })` always means \"parse hex,\"\n // even if the brand palette happens to have a key that normalizes to\n // \"ff0000\".\n const detected = detectFormat(input as ColorInput);\n let rgba: Rgba;\n if (detected !== 'UNKNOWN') {\n rgba = toRgba(input as ColorInput, detected);\n } else if (palette !== undefined && typeof input === 'string') {\n const canonical = getNameIndex(palette).get(palette.normalize(input));\n if (canonical === undefined) {\n throw new Error(\n `Unrecognized color input: ${safeStringify(input)} (not structural, and not found in palette '${palette.name}')`,\n );\n }\n const hex = (palette.colors as Record<string, HexColor>)[canonical];\n if (hex === undefined) {\n throw new Error(`Unrecognized color input: ${safeStringify(input)}`);\n }\n rgba = hexToRgba(hex);\n } else {\n throw new Error(`Unrecognized color input: ${safeStringify(input)}`);\n }\n\n // --- Emit ---\n if (format === 'NAME') {\n if (palette === undefined) {\n throw new Error(`convert(_, { format: 'NAME' }) requires a 'palette' option`);\n }\n // Palette colors are alpha = 1 by construction; a partially-transparent\n // input isn't \"that named color\" in the strict-convert sense. Reject\n // here rather than silently drop alpha during the reverse lookup.\n if (rgba.a !== 1) {\n throw new Error(\n `format: 'NAME' requires fully-opaque input (a === 1). Got a=${rgba.a}. ` +\n `Strip alpha first or use identify() for fuzzy matching.`,\n );\n }\n const hex = rgbaToHex(rgba).toLowerCase();\n const name = getReverseNameIndex(palette).get(hex);\n if (name === undefined) {\n throw new Error(\n `No exact match for ${hex} in palette '${palette.name}'. Use identify() for nearest-match semantics.`,\n );\n }\n return name;\n }\n return fromRgba(rgba, format);\n}\n"]}
package/dist/convert.js CHANGED
@@ -20,9 +20,15 @@ function safeStringify(x) {
20
20
  * Pass `knownFormat` when the caller has already run `detectFormat` to skip
21
21
  * the redundant detection. Throws on `UNKNOWN` input.
22
22
  *
23
- * Palette names (Pantone codes, BYO keys) are not accepted here — they
23
+ * Palette names (Pantone codes, BYO keys) are not accepted here; they
24
24
  * require palette data to parse. Use `convert(name, { palette })` or
25
25
  * `resolve(name, { palette })` at the higher level.
26
+ *
27
+ * @example
28
+ * toRgba('#ff0000'); // { r: 255, g: 0, b: 0, a: 1 }
29
+ * toRgba('rgb(255, 0, 0)'); // { r: 255, g: 0, b: 0, a: 1 }
30
+ * toRgba([255, 0, 0]); // { r: 255, g: 0, b: 0, a: 1 }
31
+ * toRgba({ h: 0, s: 100, l: 50 }); // { r: 255, g: 0, b: 0, a: 1 }
26
32
  */
27
33
  export function toRgba(input, knownFormat) {
28
34
  const format = knownFormat ?? detectFormat(input);
@@ -45,8 +51,13 @@ export function toRgba(input, knownFormat) {
45
51
  * Emit a canonical `Rgba` in the target structural format. Palette-independent.
46
52
  *
47
53
  * For palette-key output (e.g. nearest Pantone code, brand name), use
48
- * `convert(rgba, { format: 'NAME', palette })` — or `identify` for fuzzy,
54
+ * `convert(rgba, { format: 'NAME', palette })`; or `identify` for fuzzy,
49
55
  * `rgbaToPantone` for the low-level Pantone-only path.
56
+ *
57
+ * @example
58
+ * fromRgba({ r: 255, g: 0, b: 0, a: 1 }); // '#ff0000'
59
+ * fromRgba({ r: 255, g: 0, b: 0, a: 1 }, 'HSL'); // 'hsl(0, 100%, 50%)'
60
+ * fromRgba({ r: 255, g: 0, b: 0, a: 0.5 }, 'RGB'); // 'rgb(255, 0, 0)' (alpha dropped)
50
61
  */
51
62
  export function fromRgba(rgba, format = 'HEX') {
52
63
  switch (format) {