chromonym 3.1.1 → 3.3.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 (85) hide show
  1. package/NOTICE.md +243 -1
  2. package/README.md +51 -43
  3. package/dist/conversions/rgb.d.ts +7 -1
  4. package/dist/conversions/rgb.d.ts.map +1 -1
  5. package/dist/conversions/rgb.js +11 -2
  6. package/dist/conversions/rgb.js.map +1 -1
  7. package/dist/convert.d.ts +73 -21
  8. package/dist/convert.d.ts.map +1 -1
  9. package/dist/convert.js +13 -2
  10. package/dist/convert.js.map +1 -1
  11. package/dist/defineColorPalette.d.ts +53 -0
  12. package/dist/defineColorPalette.d.ts.map +1 -0
  13. package/dist/defineColorPalette.js +103 -0
  14. package/dist/defineColorPalette.js.map +1 -0
  15. package/dist/detectFormat.d.ts +26 -3
  16. package/dist/detectFormat.d.ts.map +1 -1
  17. package/dist/detectFormat.js +20 -0
  18. package/dist/detectFormat.js.map +1 -1
  19. package/dist/identify.d.ts +63 -17
  20. package/dist/identify.d.ts.map +1 -1
  21. package/dist/identify.js.map +1 -1
  22. package/dist/index.d.ts +13 -4
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +12 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/indexing.d.ts +13 -3
  27. package/dist/indexing.d.ts.map +1 -1
  28. package/dist/indexing.js +67 -19
  29. package/dist/indexing.js.map +1 -1
  30. package/dist/palettes/crayola.d.ts +16 -0
  31. package/dist/palettes/crayola.d.ts.map +1 -1
  32. package/dist/palettes/crayola.js +16 -0
  33. package/dist/palettes/crayola.js.map +1 -1
  34. package/dist/palettes/fs595b.d.ts +467 -0
  35. package/dist/palettes/fs595b.d.ts.map +1 -0
  36. package/dist/palettes/fs595b.js +256 -0
  37. package/dist/palettes/fs595b.js.map +1 -0
  38. package/dist/palettes/fs595c.d.ts +1224 -0
  39. package/dist/palettes/fs595c.d.ts.map +1 -0
  40. package/dist/palettes/fs595c.js +633 -0
  41. package/dist/palettes/fs595c.js.map +1 -0
  42. package/dist/palettes/isccNbs.d.ts +562 -0
  43. package/dist/palettes/isccNbs.d.ts.map +1 -0
  44. package/dist/palettes/isccNbs.js +300 -0
  45. package/dist/palettes/isccNbs.js.map +1 -0
  46. package/dist/palettes/nbs.d.ts +578 -0
  47. package/dist/palettes/nbs.d.ts.map +1 -0
  48. package/dist/palettes/nbs.js +309 -0
  49. package/dist/palettes/nbs.js.map +1 -0
  50. package/dist/palettes/ncs.d.ts +3949 -0
  51. package/dist/palettes/ncs.d.ts.map +1 -0
  52. package/dist/palettes/ncs.js +2011 -0
  53. package/dist/palettes/ncs.js.map +1 -0
  54. package/dist/palettes/ntc.d.ts +3170 -0
  55. package/dist/palettes/ntc.d.ts.map +1 -0
  56. package/dist/palettes/ntc.js +1602 -0
  57. package/dist/palettes/ntc.js.map +1 -0
  58. package/dist/palettes/pantone.d.ts +18 -1
  59. package/dist/palettes/pantone.d.ts.map +1 -1
  60. package/dist/palettes/pantone.js +18 -1
  61. package/dist/palettes/pantone.js.map +1 -1
  62. package/dist/palettes/resene.d.ts +2802 -0
  63. package/dist/palettes/resene.d.ts.map +1 -0
  64. package/dist/palettes/resene.js +1422 -0
  65. package/dist/palettes/resene.js.map +1 -0
  66. package/dist/palettes/web.d.ts +10 -0
  67. package/dist/palettes/web.d.ts.map +1 -1
  68. package/dist/palettes/web.js +10 -0
  69. package/dist/palettes/web.js.map +1 -1
  70. package/dist/palettes/x11.d.ts +15 -10
  71. package/dist/palettes/x11.d.ts.map +1 -1
  72. package/dist/palettes/x11.js +15 -10
  73. package/dist/palettes/x11.js.map +1 -1
  74. package/dist/palettes/xkcd.d.ts +1880 -0
  75. package/dist/palettes/xkcd.d.ts.map +1 -0
  76. package/dist/palettes/xkcd.js +955 -0
  77. package/dist/palettes/xkcd.js.map +1 -0
  78. package/dist/resolve.d.ts +58 -14
  79. package/dist/resolve.d.ts.map +1 -1
  80. package/dist/resolve.js +32 -16
  81. package/dist/resolve.js.map +1 -1
  82. package/dist/types.d.ts +47 -0
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/package.json +44 -5
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
@@ -14,13 +14,18 @@
14
14
  [![Types: included](https://img.shields.io/npm/types/chromonym?color=3178c6&logo=typescript)](https://www.npmjs.com/package/chromonym)
15
15
  [![CI](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml/badge.svg)](https://github.com/simiancraft/chromonym/actions/workflows/ci.yml)
16
16
  [![Coverage](https://img.shields.io/codecov/c/github/simiancraft/chromonym?logo=codecov)](https://codecov.io/github/simiancraft/chromonym)
17
- [![Bundle size](https://img.shields.io/bundlephobia/minzip/chromonym?label=bundle%20size)](https://bundlephobia.com/package/chromonym)
17
+ [![Bundle size](https://img.shields.io/badge/bundle-1.5--20%20kB%20gz-informational)](#what-you-actually-ship)
18
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/simiancraft/chromonym/badge)](https://securityscorecards.dev/viewer/?uri=github.com/simiancraft/chromonym)
18
19
 
19
20
  <p align="center">
20
21
  <code>identify</code> &nbsp;•&nbsp; <code>resolve</code> &nbsp;•&nbsp; <code>convert</code>
21
22
  </p>
22
23
 
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.
24
+ **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.
25
+
26
+ <sub>*chromonym* (n.): a name for a color; *chroma* + *-onym*. Also the library.</sub>
27
+
28
+ <sub>Every public export ships with JSDoc and a runnable `@example`; autocomplete and hover do the teaching.</sub>
24
29
 
25
30
  <p align="center">
26
31
  <a href="https://simiancraft.github.io/chromonym/">
@@ -80,18 +85,6 @@ yarn add chromonym
80
85
  npm install chromonym
81
86
  ```
82
87
 
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`](CHANGELOG.md) for the full list of v3.0 breaking changes.
94
-
95
88
  ## API
96
89
 
97
90
  Three verbs. All accept an optional `opts` object; defaults fire when omitted.
@@ -273,22 +266,24 @@ type Palette<Name extends string = string> = {
273
266
 
274
267
  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
268
 
269
+ 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`.
270
+
276
271
  ```ts
277
- import { identify, resolve, type Palette } from 'chromonym';
272
+ import { defineColorPalette, identify, resolve } from 'chromonym';
278
273
 
279
- const warhammer = {
274
+ const warhammer = defineColorPalette({
280
275
  name: 'warhammer40k',
281
276
  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',
277
+ 'world eaters red': '#8b1a1a', // hex
278
+ 'adeptus red': 'rgb(101, 32, 34)', // rgb string
279
+ 'sons of malice white': { r: 232, g: 228, b: 216 }, // rgb object
280
+ 'the flawless host purple': [107, 45, 125], // rgb tuple
281
+ 'nurgle green': 'hsl(78, 38%, 39%)', // hsl string
287
282
  'alpha legion teal': '#2a6d7a',
288
283
  },
289
- normalize: (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
284
+ normalize: (s) => s.toLowerCase().replace(/[^a-z0-9]/g, ''),
290
285
  defaultMetric: 'deltaE2000',
291
- } as const satisfies Palette;
286
+ });
292
287
 
293
288
  identify('#750c0c', { palette: warhammer })
294
289
  // → 'world eaters red' (return type narrows to the palette's key union)
@@ -297,14 +292,26 @@ resolve('Nurgle Green', { palette: warhammer })
297
292
  // → '#748c3f' (your normalizer handles case + punctuation)
298
293
  ```
299
294
 
300
- ### The four we ship
295
+ 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.
296
+
297
+ 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.
298
+
299
+ ### The twelve we ship
301
300
 
302
301
  | Name | Entries | Source |
303
302
  |---|---|---|
304
303
  | `web` (default) | 148 | CSS Color Module Level 4 named colors |
305
304
  | `x11` | 658 | X.Org `rgb.txt` (public domain) |
306
305
  | `pantone` | 907 | Pantone Coated (C) — community approximations (not Pantone-licensed) |
307
- | `crayola` | ~85 | Crayola crayon colors — community approximations (not Crayola-licensed) |
306
+ | `crayola` | 84 | Crayola crayon colors — community approximations (not Crayola-licensed) |
307
+ | `ntc` | 1566 | Chirag Mehta's "Name That Color" dataset (broad coverage of design / UI / hardware names) |
308
+ | `xkcd` | 923 | XKCD 2010 color-survey results (CC0; crowd-sourced common-speech names) |
309
+ | `fs595b` | 209 | Federal Standard 595B paint chips (1989 revision; modelers / historical equipment) |
310
+ | `fs595c` | 589 | Federal Standard 595C paint chips (US Government spec; military / aviation coatings) |
311
+ | `isccNbs` | 260 | ISCC-NBS Method of Designating Colors (1955 US standard; Munsell-partitioned named blocks) |
312
+ | `nbs` | 267 | Second digitization of the same 1955 standard; different sRGB (matches 1955 chip book; covers the 7 blocks `isccNbs` leaves as gaps) |
313
+ | `resene` | 1378 | Resene Paints (NZ) catalog; te reo Māori and NZ place-names in the naming |
314
+ | `ncs` | 1950 | Natural Colour System (Sweden) — perceptual, structured notation (`2030-R80B`), not NCS-licensed |
308
315
 
309
316
  Importable directly, or via subpath exports for stricter tree-shaking:
310
317
 
@@ -321,7 +328,7 @@ crayola.colors['Granny Smith Apple'] // '#a8e4a0'
321
328
 
322
329
  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
330
 
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.
331
+ *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
332
 
326
333
  ### Cross-palette translation
327
334
 
@@ -382,7 +389,7 @@ Rule of thumb: use `source` when you want one clean call and `null` on miss; use
382
389
 
383
390
  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
391
 
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.
392
+ > **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
393
 
387
394
  ```ts
388
395
  // Defaults: read from the palette's own `defaultMetric`.
@@ -443,6 +450,22 @@ const tryConvert = (input: ColorInput, opts = {}) => {
443
450
  - BYO palettes have zero library cost beyond your own data.
444
451
  - 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
452
 
453
+ ### What you actually ship
454
+
455
+ **Install size and bundle size are not the same number.** Install size is what lands in your `node_modules/` after `npm install`; bundle size is what ships to your users after tree-shaking, minification, and gzip. For chromonym the gap is roughly two orders of magnitude.
456
+
457
+ The published tarball is **~2.5 MB unpacked / ~557 kB gzipped** because it carries twelve palettes and ~9000 named-color entries; that's the install-size figure `packagephobia` reports. Your **production bundle** pays only for what you actually import, measured with `esbuild --minify`:
458
+
459
+ | Import | min | gzip |
460
+ |---|---|---|
461
+ | `import { defineColorPalette } from 'chromonym'` (pure BYO; no built-in) | 3.8 kB | **1.5 kB** |
462
+ | `import { identify, web } from 'chromonym'` | 10.9 kB | **4.7 kB** |
463
+ | `import { pantoneToRgba } from 'chromonym'` (single conversion) | 17.5 kB | **6.8 kB** |
464
+ | `import { identify, pantone } from 'chromonym'` | 27 kB | **11.3 kB** |
465
+ | `import { identify, ntc } from 'chromonym'` | 43 kB | **20 kB** |
466
+
467
+ If you're evaluating chromonym against a size budget, the gzip column above is what actually lands in your production build.
468
+
446
469
  ## Types
447
470
 
448
471
  Re-exported from the root barrel — `import type { ... } from 'chromonym'`:
@@ -466,17 +489,6 @@ bun test
466
489
  bun run build
467
490
  ```
468
491
 
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
492
  ## See also
481
493
 
482
494
  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 +503,4 @@ MIT © [the-simian](https://github.com/the-simian)
491
503
 
492
504
  <p align="center"><sub>Crafted with care by <a href="https://simiancraft.com">Simiancraft</a>.</sub></p>
493
505
 
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.
506
+ <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>
@@ -3,7 +3,13 @@ import type { Rgba, RgbaInput, RgbInput, RgbString } from '../types.js';
3
3
  * Normalize any RGB/RGBA input shape (string, tuple, or object) into
4
4
  * the canonical Rgba representation. Alpha defaults to 1 when absent.
5
5
  * Rejects non-finite numeric inputs; clamps r/g/b to [0,255] and a to [0,1].
6
- * Throws on malformed strings.
6
+ * Throws on malformed strings. String parsing is linear-time; no polynomial
7
+ * backtracking on pathological input. Leading-dot alpha (e.g. `.5`) is
8
+ * accepted per the CSS Color spec.
9
+ *
10
+ * @example
11
+ * rgbToRgba('rgba(255, 0, 0, .5)');
12
+ * // => { r: 255, g: 0, b: 0, a: 0.5 }
7
13
  */
8
14
  export declare function rgbToRgba(input: RgbInput | RgbaInput): Rgba;
9
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"rgb.d.ts","sourceRoot":"","sources":["../../src/conversions/rgb.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAa,SAAS,EAAE,MAAM,aAAa,CAAC;AAanF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,IAAI,CA+B3D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAK/C","sourcesContent":["import { clamp, requireFinite } from '../math/clamp.js';\nimport type { Rgba, RgbaInput, RgbInput, RgbObject, RgbString } from '../types.js';\n\nconst RGB_RE =\n /^rgba?\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*(?:,\\s*(\\d*\\.?\\d+)\\s*)?\\)$/i;\n\nfunction sanitizeChannel(n: unknown, label: string): number {\n return clamp(requireFinite(n, label), 0, 255);\n}\n\nfunction sanitizeAlpha(n: unknown): number {\n return clamp(requireFinite(n, 'alpha'), 0, 1);\n}\n\n/**\n * Normalize any RGB/RGBA input shape (string, tuple, or object) into\n * the canonical Rgba representation. Alpha defaults to 1 when absent.\n * Rejects non-finite numeric inputs; clamps r/g/b to [0,255] and a to [0,1].\n * Throws on malformed strings.\n */\nexport function rgbToRgba(input: RgbInput | RgbaInput): Rgba {\n if (Array.isArray(input)) {\n const rawA = input.length === 4 ? input[3] : 1;\n return {\n r: sanitizeChannel(input[0], 'r'),\n g: sanitizeChannel(input[1], 'g'),\n b: sanitizeChannel(input[2], 'b'),\n a: sanitizeAlpha(rawA),\n };\n }\n\n if (typeof input === 'string') {\n const match = RGB_RE.exec(input);\n if (!match) throw new Error(`Invalid rgb(a) string: ${input}`);\n return {\n r: sanitizeChannel(Number(match[1]), 'r'),\n g: sanitizeChannel(Number(match[2]), 'g'),\n b: sanitizeChannel(Number(match[3]), 'b'),\n a: sanitizeAlpha(match[4] !== undefined ? Number(match[4]) : 1),\n };\n }\n\n // Object form — TypeScript's Array.isArray narrowing doesn't exclude\n // readonly tuple types from the union, so cast after the guards.\n const obj = input as RgbObject | Rgba;\n return {\n r: sanitizeChannel(obj.r, 'r'),\n g: sanitizeChannel(obj.g, 'g'),\n b: sanitizeChannel(obj.b, 'b'),\n a: sanitizeAlpha('a' in obj ? obj.a : 1),\n };\n}\n\n/**\n * Emit an `rgb(r, g, b)` string. Alpha is dropped; values are\n * already-clamped by rgbToRgba, but we re-clamp defensively for direct\n * Rgba-object callers who bypass the input normalizer.\n */\nexport function rgbaToRgb(rgba: Rgba): RgbString {\n const r = clamp(Math.round(rgba.r), 0, 255);\n const g = clamp(Math.round(rgba.g), 0, 255);\n const b = clamp(Math.round(rgba.b), 0, 255);\n return `rgb(${r}, ${g}, ${b})` as RgbString;\n}\n"]}
1
+ {"version":3,"file":"rgb.d.ts","sourceRoot":"","sources":["../../src/conversions/rgb.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAa,SAAS,EAAE,MAAM,aAAa,CAAC;AAgBnF;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,IAAI,CA+B3D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAK/C","sourcesContent":["import { clamp, requireFinite } from '../math/clamp.js';\nimport type { Rgba, RgbaInput, RgbInput, RgbObject, RgbString } from '../types.js';\n\n// Alpha group split into two unambiguous alternatives to avoid polynomial\n// backtracking (CodeQL js/polynomial-redos) on malformed inputs like\n// `rgb(9,9,9,99...9`.\nconst RGB_RE =\n /^rgba?\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*(?:,\\s*(\\d+(?:\\.\\d+)?|\\.\\d+)\\s*)?\\)$/i;\n\nfunction sanitizeChannel(n: unknown, label: string): number {\n return clamp(requireFinite(n, label), 0, 255);\n}\n\nfunction sanitizeAlpha(n: unknown): number {\n return clamp(requireFinite(n, 'alpha'), 0, 1);\n}\n\n/**\n * Normalize any RGB/RGBA input shape (string, tuple, or object) into\n * the canonical Rgba representation. Alpha defaults to 1 when absent.\n * Rejects non-finite numeric inputs; clamps r/g/b to [0,255] and a to [0,1].\n * Throws on malformed strings. String parsing is linear-time; no polynomial\n * backtracking on pathological input. Leading-dot alpha (e.g. `.5`) is\n * accepted per the CSS Color spec.\n *\n * @example\n * rgbToRgba('rgba(255, 0, 0, .5)');\n * // => { r: 255, g: 0, b: 0, a: 0.5 }\n */\nexport function rgbToRgba(input: RgbInput | RgbaInput): Rgba {\n if (Array.isArray(input)) {\n const rawA = input.length === 4 ? input[3] : 1;\n return {\n r: sanitizeChannel(input[0], 'r'),\n g: sanitizeChannel(input[1], 'g'),\n b: sanitizeChannel(input[2], 'b'),\n a: sanitizeAlpha(rawA),\n };\n }\n\n if (typeof input === 'string') {\n const match = RGB_RE.exec(input);\n if (!match) throw new Error(`Invalid rgb(a) string: ${input}`);\n return {\n r: sanitizeChannel(Number(match[1]), 'r'),\n g: sanitizeChannel(Number(match[2]), 'g'),\n b: sanitizeChannel(Number(match[3]), 'b'),\n a: sanitizeAlpha(match[4] !== undefined ? Number(match[4]) : 1),\n };\n }\n\n // Object form — TypeScript's Array.isArray narrowing doesn't exclude\n // readonly tuple types from the union, so cast after the guards.\n const obj = input as RgbObject | Rgba;\n return {\n r: sanitizeChannel(obj.r, 'r'),\n g: sanitizeChannel(obj.g, 'g'),\n b: sanitizeChannel(obj.b, 'b'),\n a: sanitizeAlpha('a' in obj ? obj.a : 1),\n };\n}\n\n/**\n * Emit an `rgb(r, g, b)` string. Alpha is dropped; values are\n * already-clamped by rgbToRgba, but we re-clamp defensively for direct\n * Rgba-object callers who bypass the input normalizer.\n */\nexport function rgbaToRgb(rgba: Rgba): RgbString {\n const r = clamp(Math.round(rgba.r), 0, 255);\n const g = clamp(Math.round(rgba.g), 0, 255);\n const b = clamp(Math.round(rgba.b), 0, 255);\n return `rgb(${r}, ${g}, ${b})` as RgbString;\n}\n"]}
@@ -1,5 +1,8 @@
1
1
  import { clamp, requireFinite } from '../math/clamp.js';
2
- const RGB_RE = /^rgba?\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*(?:,\s*(\d*\.?\d+)\s*)?\)$/i;
2
+ // Alpha group split into two unambiguous alternatives to avoid polynomial
3
+ // backtracking (CodeQL js/polynomial-redos) on malformed inputs like
4
+ // `rgb(9,9,9,99...9`.
5
+ const RGB_RE = /^rgba?\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*(?:,\s*(\d+(?:\.\d+)?|\.\d+)\s*)?\)$/i;
3
6
  function sanitizeChannel(n, label) {
4
7
  return clamp(requireFinite(n, label), 0, 255);
5
8
  }
@@ -10,7 +13,13 @@ function sanitizeAlpha(n) {
10
13
  * Normalize any RGB/RGBA input shape (string, tuple, or object) into
11
14
  * the canonical Rgba representation. Alpha defaults to 1 when absent.
12
15
  * Rejects non-finite numeric inputs; clamps r/g/b to [0,255] and a to [0,1].
13
- * Throws on malformed strings.
16
+ * Throws on malformed strings. String parsing is linear-time; no polynomial
17
+ * backtracking on pathological input. Leading-dot alpha (e.g. `.5`) is
18
+ * accepted per the CSS Color spec.
19
+ *
20
+ * @example
21
+ * rgbToRgba('rgba(255, 0, 0, .5)');
22
+ * // => { r: 255, g: 0, b: 0, a: 0.5 }
14
23
  */
15
24
  export function rgbToRgba(input) {
16
25
  if (Array.isArray(input)) {
@@ -1 +1 @@
1
- {"version":3,"file":"rgb.js","sourceRoot":"","sources":["../../src/conversions/rgb.ts"],"names":[],"mappings":"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","sourcesContent":["import { clamp, requireFinite } from '../math/clamp.js';\nimport type { Rgba, RgbaInput, RgbInput, RgbObject, RgbString } from '../types.js';\n\nconst RGB_RE =\n /^rgba?\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*(?:,\\s*(\\d*\\.?\\d+)\\s*)?\\)$/i;\n\nfunction sanitizeChannel(n: unknown, label: string): number {\n return clamp(requireFinite(n, label), 0, 255);\n}\n\nfunction sanitizeAlpha(n: unknown): number {\n return clamp(requireFinite(n, 'alpha'), 0, 1);\n}\n\n/**\n * Normalize any RGB/RGBA input shape (string, tuple, or object) into\n * the canonical Rgba representation. Alpha defaults to 1 when absent.\n * Rejects non-finite numeric inputs; clamps r/g/b to [0,255] and a to [0,1].\n * Throws on malformed strings.\n */\nexport function rgbToRgba(input: RgbInput | RgbaInput): Rgba {\n if (Array.isArray(input)) {\n const rawA = input.length === 4 ? input[3] : 1;\n return {\n r: sanitizeChannel(input[0], 'r'),\n g: sanitizeChannel(input[1], 'g'),\n b: sanitizeChannel(input[2], 'b'),\n a: sanitizeAlpha(rawA),\n };\n }\n\n if (typeof input === 'string') {\n const match = RGB_RE.exec(input);\n if (!match) throw new Error(`Invalid rgb(a) string: ${input}`);\n return {\n r: sanitizeChannel(Number(match[1]), 'r'),\n g: sanitizeChannel(Number(match[2]), 'g'),\n b: sanitizeChannel(Number(match[3]), 'b'),\n a: sanitizeAlpha(match[4] !== undefined ? Number(match[4]) : 1),\n };\n }\n\n // Object form — TypeScript's Array.isArray narrowing doesn't exclude\n // readonly tuple types from the union, so cast after the guards.\n const obj = input as RgbObject | Rgba;\n return {\n r: sanitizeChannel(obj.r, 'r'),\n g: sanitizeChannel(obj.g, 'g'),\n b: sanitizeChannel(obj.b, 'b'),\n a: sanitizeAlpha('a' in obj ? obj.a : 1),\n };\n}\n\n/**\n * Emit an `rgb(r, g, b)` string. Alpha is dropped; values are\n * already-clamped by rgbToRgba, but we re-clamp defensively for direct\n * Rgba-object callers who bypass the input normalizer.\n */\nexport function rgbaToRgb(rgba: Rgba): RgbString {\n const r = clamp(Math.round(rgba.r), 0, 255);\n const g = clamp(Math.round(rgba.g), 0, 255);\n const b = clamp(Math.round(rgba.b), 0, 255);\n return `rgb(${r}, ${g}, ${b})` as RgbString;\n}\n"]}
1
+ {"version":3,"file":"rgb.js","sourceRoot":"","sources":["../../src/conversions/rgb.ts"],"names":[],"mappings":"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","sourcesContent":["import { clamp, requireFinite } from '../math/clamp.js';\nimport type { Rgba, RgbaInput, RgbInput, RgbObject, RgbString } from '../types.js';\n\n// Alpha group split into two unambiguous alternatives to avoid polynomial\n// backtracking (CodeQL js/polynomial-redos) on malformed inputs like\n// `rgb(9,9,9,99...9`.\nconst RGB_RE =\n /^rgba?\\s*\\(\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*(?:,\\s*(\\d+(?:\\.\\d+)?|\\.\\d+)\\s*)?\\)$/i;\n\nfunction sanitizeChannel(n: unknown, label: string): number {\n return clamp(requireFinite(n, label), 0, 255);\n}\n\nfunction sanitizeAlpha(n: unknown): number {\n return clamp(requireFinite(n, 'alpha'), 0, 1);\n}\n\n/**\n * Normalize any RGB/RGBA input shape (string, tuple, or object) into\n * the canonical Rgba representation. Alpha defaults to 1 when absent.\n * Rejects non-finite numeric inputs; clamps r/g/b to [0,255] and a to [0,1].\n * Throws on malformed strings. String parsing is linear-time; no polynomial\n * backtracking on pathological input. Leading-dot alpha (e.g. `.5`) is\n * accepted per the CSS Color spec.\n *\n * @example\n * rgbToRgba('rgba(255, 0, 0, .5)');\n * // => { r: 255, g: 0, b: 0, a: 0.5 }\n */\nexport function rgbToRgba(input: RgbInput | RgbaInput): Rgba {\n if (Array.isArray(input)) {\n const rawA = input.length === 4 ? input[3] : 1;\n return {\n r: sanitizeChannel(input[0], 'r'),\n g: sanitizeChannel(input[1], 'g'),\n b: sanitizeChannel(input[2], 'b'),\n a: sanitizeAlpha(rawA),\n };\n }\n\n if (typeof input === 'string') {\n const match = RGB_RE.exec(input);\n if (!match) throw new Error(`Invalid rgb(a) string: ${input}`);\n return {\n r: sanitizeChannel(Number(match[1]), 'r'),\n g: sanitizeChannel(Number(match[2]), 'g'),\n b: sanitizeChannel(Number(match[3]), 'b'),\n a: sanitizeAlpha(match[4] !== undefined ? Number(match[4]) : 1),\n };\n }\n\n // Object form — TypeScript's Array.isArray narrowing doesn't exclude\n // readonly tuple types from the union, so cast after the guards.\n const obj = input as RgbObject | Rgba;\n return {\n r: sanitizeChannel(obj.r, 'r'),\n g: sanitizeChannel(obj.g, 'g'),\n b: sanitizeChannel(obj.b, 'b'),\n a: sanitizeAlpha('a' in obj ? obj.a : 1),\n };\n}\n\n/**\n * Emit an `rgb(r, g, b)` string. Alpha is dropped; values are\n * already-clamped by rgbToRgba, but we re-clamp defensively for direct\n * Rgba-object callers who bypass the input normalizer.\n */\nexport function rgbaToRgb(rgba: Rgba): RgbString {\n const r = clamp(Math.round(rgba.r), 0, 255);\n const g = clamp(Math.round(rgba.g), 0, 255);\n const b = clamp(Math.round(rgba.b), 0, 255);\n return `rgb(${r}, ${g}, ${b})` as RgbString;\n}\n"]}
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