@carto/ps-utils 2.0.1 → 2.1.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 (46) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +436 -74
  4. package/dist/index.js.map +1 -1
  5. package/dist/types/clipboard/copy.d.ts +4 -6
  6. package/dist/types/colors/carto-palettes/carto-palettes.d.ts +47 -0
  7. package/dist/types/colors/hex-to-rgba/hex-to-rgba.d.ts +7 -16
  8. package/dist/types/colors/palette-to-hex/palette-to-hex.d.ts +19 -0
  9. package/dist/types/colors/palette-to-hex/palette-to-hex.test.d.ts +1 -0
  10. package/dist/types/colors/rgba-to-hex/rgba-to-hex.d.ts +7 -19
  11. package/dist/types/debounce/debounce.d.ts +12 -2
  12. package/dist/types/formatters/constants.d.ts +23 -1
  13. package/dist/types/formatters/date/format-date.d.ts +13 -8
  14. package/dist/types/formatters/number/format-number.d.ts +17 -20
  15. package/dist/types/index.d.ts +3 -0
  16. package/dist/types/strings/match-text/match-text.d.ts +10 -6
  17. package/dist/types/strings/normalize/normalize.d.ts +6 -9
  18. package/dist/types/strings/routes/replace-route.d.ts +10 -14
  19. package/package.json +7 -3
  20. package/src/clipboard/copy.test.ts +49 -0
  21. package/src/clipboard/copy.ts +18 -0
  22. package/src/colors/carto-palettes/carto-palettes.ts +355 -0
  23. package/src/colors/hex-to-rgba/hex-to-rgba.test.ts +82 -0
  24. package/src/colors/hex-to-rgba/hex-to-rgba.ts +67 -0
  25. package/src/colors/palette-to-hex/palette-to-hex.test.ts +60 -0
  26. package/src/colors/palette-to-hex/palette-to-hex.ts +45 -0
  27. package/src/colors/rgba-to-hex/rgba-to-hex.test.ts +34 -0
  28. package/src/colors/rgba-to-hex/rgba-to-hex.ts +42 -0
  29. package/src/colors/rgba-to-hex/types.ts +9 -0
  30. package/src/debounce/debounce.test.ts +21 -0
  31. package/src/debounce/debounce.ts +29 -0
  32. package/src/formatters/constants.ts +49 -0
  33. package/src/formatters/date/format-date.test.ts +36 -0
  34. package/src/formatters/date/format-date.ts +34 -0
  35. package/src/formatters/number/format-number.test.ts +49 -0
  36. package/src/formatters/number/format-number.ts +68 -0
  37. package/src/index.ts +18 -0
  38. package/src/strings/match-text/match-text.test.ts +38 -0
  39. package/src/strings/match-text/match-text.ts +36 -0
  40. package/src/strings/match-text/types.ts +15 -0
  41. package/src/strings/normalize/normalize.test.ts +12 -0
  42. package/src/strings/normalize/normalize.ts +22 -0
  43. package/src/strings/normalize/types.ts +15 -0
  44. package/src/strings/routes/replace-route.test.ts +64 -0
  45. package/src/strings/routes/replace-route.ts +39 -0
  46. package/src/strings/routes/types.ts +2 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * CARTOColors palettes (name -> hex ramp), full CARTO-native set: sequential
3
+ * (incl. aggregation), diverging and qualitative. Each entry is the palette's
4
+ * largest class-count ramp, verbatim. Kept inline so the package stays
5
+ * zero-dependency; ColorBrewer (`cb_*`) palettes are intentionally excluded.
6
+ *
7
+ * Data: CARTOColors - https://github.com/CartoDB/cartocolor - CC-BY-4.0.
8
+ * Generated from cartocolor@5.0.2 (unmodified beyond taking each max ramp).
9
+ */
10
+ export const CARTO_PALETTES = {
11
+ // Sequential (+ aggregation)
12
+ ag_GrnYl: [
13
+ '#245668',
14
+ '#0F7279',
15
+ '#0D8F81',
16
+ '#39AB7E',
17
+ '#6EC574',
18
+ '#A9DC67',
19
+ '#EDEF5D',
20
+ ],
21
+ ag_Sunset: [
22
+ '#4b2991',
23
+ '#872ca2',
24
+ '#c0369d',
25
+ '#ea4f88',
26
+ '#fa7876',
27
+ '#f6a97a',
28
+ '#edd9a3',
29
+ ],
30
+ BluGrn: [
31
+ '#c4e6c3',
32
+ '#96d2a4',
33
+ '#6dbc90',
34
+ '#4da284',
35
+ '#36877a',
36
+ '#266b6e',
37
+ '#1d4f60',
38
+ ],
39
+ BluYl: [
40
+ '#f7feae',
41
+ '#b7e6a5',
42
+ '#7ccba2',
43
+ '#46aea0',
44
+ '#089099',
45
+ '#00718b',
46
+ '#045275',
47
+ ],
48
+ BrwnYl: [
49
+ '#ede5cf',
50
+ '#e0c2a2',
51
+ '#d39c83',
52
+ '#c1766f',
53
+ '#a65461',
54
+ '#813753',
55
+ '#541f3f',
56
+ ],
57
+ Burg: [
58
+ '#ffc6c4',
59
+ '#f4a3a8',
60
+ '#e38191',
61
+ '#cc607d',
62
+ '#ad466c',
63
+ '#8b3058',
64
+ '#672044',
65
+ ],
66
+ BurgYl: [
67
+ '#fbe6c5',
68
+ '#f5ba98',
69
+ '#ee8a82',
70
+ '#dc7176',
71
+ '#c8586c',
72
+ '#9c3f5d',
73
+ '#70284a',
74
+ ],
75
+ DarkMint: [
76
+ '#d2fbd4',
77
+ '#a5dbc2',
78
+ '#7bbcb0',
79
+ '#559c9e',
80
+ '#3a7c89',
81
+ '#235d72',
82
+ '#123f5a',
83
+ ],
84
+ Emrld: [
85
+ '#d3f2a3',
86
+ '#97e196',
87
+ '#6cc08b',
88
+ '#4c9b82',
89
+ '#217a79',
90
+ '#105965',
91
+ '#074050',
92
+ ],
93
+ Magenta: [
94
+ '#f3cbd3',
95
+ '#eaa9bd',
96
+ '#dd88ac',
97
+ '#ca699d',
98
+ '#b14d8e',
99
+ '#91357d',
100
+ '#6c2167',
101
+ ],
102
+ Mint: [
103
+ '#e4f1e1',
104
+ '#b4d9cc',
105
+ '#89c0b6',
106
+ '#63a6a0',
107
+ '#448c8a',
108
+ '#287274',
109
+ '#0d585f',
110
+ ],
111
+ OrYel: [
112
+ '#ecda9a',
113
+ '#efc47e',
114
+ '#f3ad6a',
115
+ '#f7945d',
116
+ '#f97b57',
117
+ '#f66356',
118
+ '#ee4d5a',
119
+ ],
120
+ Peach: [
121
+ '#fde0c5',
122
+ '#facba6',
123
+ '#f8b58b',
124
+ '#f59e72',
125
+ '#f2855d',
126
+ '#ef6a4c',
127
+ '#eb4a40',
128
+ ],
129
+ PinkYl: [
130
+ '#fef6b5',
131
+ '#ffdd9a',
132
+ '#ffc285',
133
+ '#ffa679',
134
+ '#fa8a76',
135
+ '#f16d7a',
136
+ '#e15383',
137
+ ],
138
+ Purp: [
139
+ '#f3e0f7',
140
+ '#e4c7f1',
141
+ '#d1afe8',
142
+ '#b998dd',
143
+ '#9f82ce',
144
+ '#826dba',
145
+ '#63589f',
146
+ ],
147
+ PurpOr: [
148
+ '#f9ddda',
149
+ '#f2b9c4',
150
+ '#e597b9',
151
+ '#ce78b3',
152
+ '#ad5fad',
153
+ '#834ba0',
154
+ '#573b88',
155
+ ],
156
+ RedOr: [
157
+ '#f6d2a9',
158
+ '#f5b78e',
159
+ '#f19c7c',
160
+ '#ea8171',
161
+ '#dd686c',
162
+ '#ca5268',
163
+ '#b13f64',
164
+ ],
165
+ Sunset: [
166
+ '#f3e79b',
167
+ '#fac484',
168
+ '#f8a07e',
169
+ '#eb7f86',
170
+ '#ce6693',
171
+ '#a059a0',
172
+ '#5c53a5',
173
+ ],
174
+ SunsetDark: [
175
+ '#fcde9c',
176
+ '#faa476',
177
+ '#f0746e',
178
+ '#e34f6f',
179
+ '#dc3977',
180
+ '#b9257a',
181
+ '#7c1d6f',
182
+ ],
183
+ Teal: [
184
+ '#d1eeea',
185
+ '#a8dbd9',
186
+ '#85c4c9',
187
+ '#68abb8',
188
+ '#4f90a6',
189
+ '#3b738f',
190
+ '#2a5674',
191
+ ],
192
+ TealGrn: [
193
+ '#b0f2bc',
194
+ '#89e8ac',
195
+ '#67dba5',
196
+ '#4cc8a3',
197
+ '#38b2a3',
198
+ '#2c98a0',
199
+ '#257d98',
200
+ ],
201
+
202
+ // Diverging
203
+ ArmyRose: [
204
+ '#798234',
205
+ '#a3ad62',
206
+ '#d0d3a2',
207
+ '#fdfbe4',
208
+ '#f0c6c3',
209
+ '#df91a3',
210
+ '#d46780',
211
+ ],
212
+ Earth: [
213
+ '#A16928',
214
+ '#bd925a',
215
+ '#d6bd8d',
216
+ '#edeac2',
217
+ '#b5c8b8',
218
+ '#79a7ac',
219
+ '#2887a1',
220
+ ],
221
+ Fall: [
222
+ '#3d5941',
223
+ '#778868',
224
+ '#b5b991',
225
+ '#f6edbd',
226
+ '#edbb8a',
227
+ '#de8a5a',
228
+ '#ca562c',
229
+ ],
230
+ Geyser: [
231
+ '#008080',
232
+ '#70a494',
233
+ '#b4c8a8',
234
+ '#f6edbd',
235
+ '#edbb8a',
236
+ '#de8a5a',
237
+ '#ca562c',
238
+ ],
239
+ TealRose: [
240
+ '#009392',
241
+ '#72aaa1',
242
+ '#b1c7b3',
243
+ '#f1eac8',
244
+ '#e5b9ad',
245
+ '#d98994',
246
+ '#d0587e',
247
+ ],
248
+ Temps: [
249
+ '#009392',
250
+ '#39b185',
251
+ '#9ccb86',
252
+ '#e9e29c',
253
+ '#eeb479',
254
+ '#e88471',
255
+ '#cf597e',
256
+ ],
257
+ Tropic: [
258
+ '#009B9E',
259
+ '#42B7B9',
260
+ '#A7D3D4',
261
+ '#F1F1F1',
262
+ '#E4C1D9',
263
+ '#D691C1',
264
+ '#C75DAB',
265
+ ],
266
+
267
+ // Qualitative
268
+ Antique: [
269
+ '#855C75',
270
+ '#D9AF6B',
271
+ '#AF6458',
272
+ '#736F4C',
273
+ '#526A83',
274
+ '#625377',
275
+ '#68855C',
276
+ '#9C9C5E',
277
+ '#A06177',
278
+ '#8C785D',
279
+ '#467378',
280
+ '#7C7C7C',
281
+ ],
282
+ Bold: [
283
+ '#7F3C8D',
284
+ '#11A579',
285
+ '#3969AC',
286
+ '#F2B701',
287
+ '#E73F74',
288
+ '#80BA5A',
289
+ '#E68310',
290
+ '#008695',
291
+ '#CF1C90',
292
+ '#f97b72',
293
+ '#4b4b8f',
294
+ '#A5AA99',
295
+ ],
296
+ Pastel: [
297
+ '#66C5CC',
298
+ '#F6CF71',
299
+ '#F89C74',
300
+ '#DCB0F2',
301
+ '#87C55F',
302
+ '#9EB9F3',
303
+ '#FE88B1',
304
+ '#C9DB74',
305
+ '#8BE0A4',
306
+ '#B497E7',
307
+ '#D3B484',
308
+ '#B3B3B3',
309
+ ],
310
+ Prism: [
311
+ '#5F4690',
312
+ '#1D6996',
313
+ '#38A6A5',
314
+ '#0F8554',
315
+ '#73AF48',
316
+ '#EDAD08',
317
+ '#E17C05',
318
+ '#CC503E',
319
+ '#94346E',
320
+ '#6F4070',
321
+ '#994E95',
322
+ '#666666',
323
+ ],
324
+ Safe: [
325
+ '#88CCEE',
326
+ '#CC6677',
327
+ '#DDCC77',
328
+ '#117733',
329
+ '#332288',
330
+ '#AA4499',
331
+ '#44AA99',
332
+ '#999933',
333
+ '#882255',
334
+ '#661100',
335
+ '#6699CC',
336
+ '#888888',
337
+ ],
338
+ Vivid: [
339
+ '#E58606',
340
+ '#5D69B1',
341
+ '#52BCA3',
342
+ '#99C945',
343
+ '#CC61B0',
344
+ '#24796C',
345
+ '#DAA51B',
346
+ '#2F8AC4',
347
+ '#764E9F',
348
+ '#ED645A',
349
+ '#CC3A8E',
350
+ '#A5AA99',
351
+ ],
352
+ } satisfies Record<string, readonly string[]>
353
+
354
+ /** A known CARTOColors palette name. */
355
+ export type PaletteName = keyof typeof CARTO_PALETTES
@@ -0,0 +1,82 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+ import { hexToRgba } from './hex-to-rgba'
3
+
4
+ describe.each([
5
+ { hex: '#000000', op: 0, expected: [0, 0, 0, 0] },
6
+ { hex: '#000', op: 0, expected: [0, 0, 0, 0] },
7
+ { hex: '#00', op: 0, expected: [0, 0, 0, 0] },
8
+ { hex: '000000', op: 0, expected: [0, 0, 0, 0] },
9
+ { hex: '000', op: 0, expected: [0, 0, 0, 0] },
10
+ { hex: '00', op: 0, expected: [0, 0, 0, 0] },
11
+ { hex: '#ff', op: 0, expected: [255, 255, 255, 0] },
12
+ { hex: '#fff', op: 0, expected: [255, 255, 255, 0] },
13
+ { hex: '#ffffff', op: 0, expected: [255, 255, 255, 0] },
14
+ { hex: '#ffffff', op: 0.5, expected: [255, 255, 255, 127.5] },
15
+ { hex: '#ffffff', op: 0.1, expected: [255, 255, 255, 25.5] },
16
+ { hex: '#ffffff', op: 0.75, expected: [255, 255, 255, 191.25] },
17
+ { hex: 'ffffff', op: 0.75, expected: [255, 255, 255, 191.25] },
18
+ { hex: '#abc', op: 1, expected: [171, 202, 188, 255] },
19
+ { hex: 'abc', op: 0.5, expected: [171, 202, 188, 127.5] },
20
+ { hex: '#123456', op: 1, expected: [18, 52, 86, 255] },
21
+ { hex: '123456', op: 0, expected: [18, 52, 86, 0] },
22
+ { hex: '#a1', op: 1, expected: [161, 161, 161, 255] },
23
+ { hex: 'a1', op: 0.5, expected: [161, 161, 161, 127.5] },
24
+ ])('hexToRgba(%s, %i)', ({ hex, op, expected }) => {
25
+ test(`returns ${JSON.stringify(expected)}`, () => {
26
+ expect(hexToRgba(hex, op)).toEqual(expected)
27
+ })
28
+ })
29
+
30
+ describe.each([
31
+ { hex: '#000000', op: -1, error: 'hexToRgba: -1 invalid alpha format' },
32
+ {
33
+ hex: '#000000',
34
+ op: 1.1,
35
+ error: 'hexToRgba: 1.1 invalid alpha format',
36
+ },
37
+ {
38
+ hex: 'null',
39
+ op: 0,
40
+ error: 'hexToRgba: null invalid hexadecimal format',
41
+ },
42
+ {
43
+ hex: null,
44
+ op: 0,
45
+ error: 'hexToRgba: null invalid hexadecimal format',
46
+ },
47
+ {
48
+ hex: 'nul',
49
+ op: 0,
50
+ error: 'hexToRgba: nul invalid hexadecimal format',
51
+ },
52
+ {
53
+ hex: '',
54
+ op: 0,
55
+ error: 'hexToRgba: invalid hexadecimal format',
56
+ },
57
+ {
58
+ hex: 'gggggg',
59
+ op: 1,
60
+ error: 'hexToRgba: gggggg invalid hexadecimal format',
61
+ },
62
+ ])('hexToRgba fails', ({ hex, op, error }) => {
63
+ test(`returns ${error}`, () => {
64
+ const err = vi.spyOn(console, 'warn').mockImplementation(() => vi.fn())
65
+ expect(hexToRgba(hex!, op)).toBeUndefined()
66
+ expect(err).toHaveBeenLastCalledWith(error)
67
+ })
68
+ })
69
+
70
+ describe('hexToRgba with default alpha', () => {
71
+ test('should use default alpha value of 1', () => {
72
+ expect(hexToRgba('#ffffff')).toEqual([255, 255, 255, 255])
73
+ })
74
+
75
+ test('should use default alpha value of 1 for 3-digit hex', () => {
76
+ expect(hexToRgba('#abc')).toEqual([171, 202, 188, 255])
77
+ })
78
+
79
+ test('should use default alpha value of 1 for 2-digit hex', () => {
80
+ expect(hexToRgba('#ff')).toEqual([255, 255, 255, 255])
81
+ })
82
+ })
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Converts a hexadecimal color string to an RGBA array, useful for deck.gl layer styling and canvas operations.
3
+ *
4
+ * @param hexadecimal - Hex color string (supports `#ffffff`, `ffffff`, `#fff`, `fff`, `#ff` formats).
5
+ * @param alpha - Opacity level from 0 (transparent) to 1 (opaque). Default is `1`.
6
+ * @returns An `[r, g, b, a]` array with values 0-255, or `undefined` for invalid inputs.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * hexToRgba('#ff0000', 0.8) // [255, 0, 0, 204]
11
+ * hexToRgba('#f00', 0.5) // [255, 0, 0, 127.5]
12
+ * hexToRgba('00ff00') // [0, 255, 0, 255]
13
+ * ```
14
+ */
15
+ export function hexToRgba(
16
+ hexadecimal: string,
17
+ alpha = 1,
18
+ ): [number, number, number, number] | undefined {
19
+ if (!hexadecimal) {
20
+ // eslint-disable-next-line no-console
21
+ console.warn(`hexToRgba: ${hexadecimal} invalid hexadecimal format`)
22
+ return
23
+ }
24
+ if (alpha < 0 || alpha > 1) {
25
+ // eslint-disable-next-line no-console
26
+ console.warn(`hexToRgba: ${alpha} invalid alpha format`)
27
+ return
28
+ }
29
+ if (hexadecimal?.includes('#')) {
30
+ hexadecimal = hexadecimal.slice(1)
31
+ }
32
+ if (
33
+ !/[0-9A-Fa-f]{6}/g.test(hexadecimal) &&
34
+ !/[0-9A-Fa-f]{3}/g.test(hexadecimal) &&
35
+ !/[0-9A-Fa-f]{2}/g.test(hexadecimal)
36
+ ) {
37
+ // eslint-disable-next-line no-console
38
+ console.warn(`hexToRgba: ${hexadecimal} invalid hexadecimal format`)
39
+ return
40
+ }
41
+
42
+ const length = hexadecimal.length
43
+
44
+ if (length < 6) {
45
+ hexadecimal = hexadecimal.slice(0, 6 - length).repeat(6 / length)
46
+ }
47
+
48
+ const r = parseInt(
49
+ hexadecimal.length === 3
50
+ ? hexadecimal.slice(0, 1).repeat(2)
51
+ : hexadecimal.slice(0, 2),
52
+ 16,
53
+ )
54
+ const g = parseInt(
55
+ hexadecimal.length === 3
56
+ ? hexadecimal.slice(1, 2).repeat(2)
57
+ : hexadecimal.slice(2, 4),
58
+ 16,
59
+ )
60
+ const b = parseInt(
61
+ hexadecimal.length === 3
62
+ ? hexadecimal.slice(2, 3).repeat(2)
63
+ : hexadecimal.slice(4, 6),
64
+ 16,
65
+ )
66
+ return [r, g, b, alpha * 255]
67
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+ import { CARTO_PALETTES } from '../carto-palettes/carto-palettes'
3
+ import { paletteToHex } from './palette-to-hex'
4
+
5
+ const SUNSET = [
6
+ '#f3e79b',
7
+ '#fac484',
8
+ '#f8a07e',
9
+ '#eb7f86',
10
+ '#ce6693',
11
+ '#a059a0',
12
+ '#5c53a5',
13
+ ]
14
+
15
+ describe('paletteToHex', () => {
16
+ test('returns the palette ramp in order', () => {
17
+ expect(paletteToHex('Sunset')).toEqual(SUNSET)
18
+ })
19
+
20
+ test('resolves the name case-insensitively', () => {
21
+ expect(paletteToHex('sunset')).toEqual(SUNSET)
22
+ expect(paletteToHex('SUNSET')).toEqual(SUNSET)
23
+ expect(paletteToHex('sUnSeT')).toEqual(SUNSET)
24
+ })
25
+
26
+ test('returns the full ramp for a qualitative palette', () => {
27
+ // Bold carries 12 categorical colors, longer than the seq/div 7.
28
+ expect(paletteToHex('Bold')).toHaveLength(12)
29
+ expect(paletteToHex('Bold')).toEqual([...CARTO_PALETTES.Bold])
30
+ })
31
+
32
+ test('returns undefined and warns for an unknown palette', () => {
33
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
34
+ expect(paletteToHex('nope')).toBeUndefined()
35
+ expect(warn).toHaveBeenLastCalledWith('paletteToHex: nope unknown palette')
36
+ warn.mockRestore()
37
+ })
38
+
39
+ test('does not resolve inherited object keys', () => {
40
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
41
+ for (const key of ['__proto__', 'constructor', 'toString']) {
42
+ expect(paletteToHex(key)).toBeUndefined()
43
+ }
44
+ warn.mockRestore()
45
+ })
46
+
47
+ test('returns a fresh copy that does not mutate the shared map', () => {
48
+ const ramp = paletteToHex('Sunset')!
49
+ ramp[0] = '#000000'
50
+ ramp.reverse()
51
+ expect(CARTO_PALETTES.Sunset).toEqual(SUNSET)
52
+ })
53
+
54
+ test('has no case-insensitive name collisions', () => {
55
+ const lowered = new Set(
56
+ Object.keys(CARTO_PALETTES).map((n) => n.toLowerCase()),
57
+ )
58
+ expect(lowered.size).toBe(Object.keys(CARTO_PALETTES).length)
59
+ })
60
+ })
@@ -0,0 +1,45 @@
1
+ import {
2
+ CARTO_PALETTES,
3
+ type PaletteName,
4
+ } from '../carto-palettes/carto-palettes'
5
+
6
+ /** Lowercased name → ramp, for case-insensitive resolution. */
7
+ const PALETTES_BY_LOWER = new Map<string, readonly string[]>(
8
+ Object.entries(CARTO_PALETTES).map(([name, colors]) => [
9
+ name.toLowerCase(),
10
+ colors,
11
+ ]),
12
+ )
13
+
14
+ /**
15
+ * Resolves a CARTOColors palette name to its hex ramp.
16
+ *
17
+ * Matching is case-insensitive (`'sunset'` resolves `'Sunset'`). Returns the
18
+ * palette's largest class-count ramp. To get RGBA tuples, compose with
19
+ * `hexToRgba`: `paletteToHex('Sunset')?.map((hex) => hexToRgba(hex))`.
20
+ *
21
+ * @param name - CARTOColors palette name (e.g. `'Sunset'`), any casing.
22
+ * @returns A fresh `string[]` of hex stops (a copy — safe to mutate), or
23
+ * `undefined` for an unknown palette.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * paletteToHex('Sunset') // ['#f3e79b', …, '#5c53a5']
28
+ * paletteToHex('sunset') // ['#f3e79b', …, '#5c53a5']
29
+ * ```
30
+ */
31
+ export function paletteToHex(
32
+ // `PaletteName | (string & {})` keeps autocomplete for known names while
33
+ // still accepting arbitrary casing (which isn't a `PaletteName` literal).
34
+ name: PaletteName | (string & {}),
35
+ ): string[] | undefined {
36
+ // Static snapshot of cartocolor's max class-count ramp per palette; if
37
+ // per-class-count ramps are ever needed, source them from `cartocolor`.
38
+ const hexes = PALETTES_BY_LOWER.get(name.toLowerCase())
39
+ if (!hexes) {
40
+ // eslint-disable-next-line no-console
41
+ console.warn(`paletteToHex: ${name} unknown palette`)
42
+ return
43
+ }
44
+ return hexes.slice()
45
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+ import { rgbaToHex } from './rgba-to-hex'
3
+
4
+ describe.each([
5
+ { rgba: [0, 0, 0, 255], expected: '#000000ff' },
6
+ { rgba: [255, 255, 255, 0], expected: '#ffffff00' },
7
+ { rgba: [255, 255, 255, 127], expected: '#ffffff7f' },
8
+ { rgba: [255, 255, 255], options: { withPrefix: false }, expected: 'ffffff' },
9
+ ])('rgbaToHex(%s, %i)', ({ rgba, options, expected }) => {
10
+ test(`returns ${expected}`, () => {
11
+ expect(rgbaToHex(rgba, options)).toEqual(expected)
12
+ })
13
+ })
14
+
15
+ describe.each([
16
+ {
17
+ rgba: undefined,
18
+ error: 'rgbaToHex: invalid array or empty values',
19
+ },
20
+ {
21
+ rgba: [],
22
+ error: 'rgbaToHex: invalid array or empty values',
23
+ },
24
+ {
25
+ rgba: [0, -1, 0],
26
+ error: 'rgbaToHex: [0,-1,0] invalid array value format',
27
+ },
28
+ ])('rgbaToHex fails', ({ rgba, error }) => {
29
+ test(`returns ${error}`, () => {
30
+ const err = vi.spyOn(console, 'warn').mockImplementation(() => vi.fn())
31
+ expect(rgbaToHex(rgba)).toBeUndefined()
32
+ expect(err).toHaveBeenLastCalledWith(error)
33
+ })
34
+ })
@@ -0,0 +1,42 @@
1
+ import type { RgbaToHexOptions } from './types'
2
+
3
+ /**
4
+ * Converts an RGBA color array to a hexadecimal string, ideal for converting deck.gl colors to CSS-compatible format.
5
+ *
6
+ * @param rgba - Array of RGBA values (each 0-255). Accepts `[r, g, b]` or `[r, g, b, a]`.
7
+ * @param options - Options including `withPrefix` to control `#` prefix (default: `true`).
8
+ * @returns The hex color string, or `undefined` for invalid inputs.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * rgbaToHex([255, 0, 0]) // '#ff0000'
13
+ * rgbaToHex([255, 0, 0, 128]) // '#ff000080'
14
+ * rgbaToHex([0, 255, 0], { withPrefix: false }) // '00ff00'
15
+ * ```
16
+ */
17
+ export function rgbaToHex(
18
+ rgba: number[] | undefined,
19
+ { withPrefix = true }: RgbaToHexOptions = {},
20
+ ): string | undefined {
21
+ if (!rgba?.length) {
22
+ // eslint-disable-next-line no-console
23
+ console.warn(`rgbaToHex: invalid array or empty values`)
24
+ return
25
+ }
26
+
27
+ const hasInvalidValue = rgba.some((value) => value < 0 || value > 255)
28
+ if (hasInvalidValue) {
29
+ // eslint-disable-next-line no-console
30
+ console.warn(`rgbaToHex: [${rgba.toString()}] invalid array value format`)
31
+ return
32
+ }
33
+
34
+ const hexadecimal = rgba
35
+ .map((value) => {
36
+ const hex = value.toString(16)
37
+ return hex.padStart(2, '0')
38
+ })
39
+ .join('')
40
+
41
+ return `${withPrefix ? '#' : ''}${hexadecimal}`
42
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `RgbaToHexOptions` is an object with an optional property `withPrefix` of type
3
+ * `boolean`.
4
+ * @property {boolean} withPrefix - boolean - Whether to include the '#' prefix in the hex
5
+ * string.
6
+ */
7
+ export interface RgbaToHexOptions {
8
+ withPrefix?: boolean
9
+ }