From 5551d9252837b913e06fb937d091a88520291471 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 06:47:38 +0000 Subject: [PATCH 1/3] Replace colorjs.io with @colordx/core colordx's tree-shakeable plugin architecture (lab, lch, hwb, hsv, p3, rec2020, a98rgb, prophoto, names) covers the same color spaces the previous colorjs.io setup registered, minus `color(srgb ...)` / `color(srgb-linear ...)` function-syntax parsing, which @colordx/core doesn't support yet. Cuts the real-world (bundled + minified + gzipped) footprint of this package from ~23.4 KB to ~12.7 KB. --- index.test.ts | 29 +++++++++++++++++ index.ts | 87 +++++++++++++------------------------------------- package.json | 2 +- pnpm-lock.yaml | 16 +++++----- 4 files changed, 60 insertions(+), 74 deletions(-) diff --git a/index.test.ts b/index.test.ts index 51dce54..a0f658e 100644 --- a/index.test.ts +++ b/index.test.ts @@ -51,6 +51,35 @@ describe('convert', () => { } }) + test('the convert fn parses wide-gamut and CIE color spaces', () => { + // All of these are equivalent to `red` (#ff0000) + const colors = [ + 'lab(54.2905414047 80.8049283205 69.8909647686)', + 'lch(54.2905414047 106.8371817167 40.8576564524)', + 'hwb(0 0% 0%)', + 'color(display-p3 0.9174875573 0.2002868079 0.1385605911)', + 'color(a98-rgb 0.8585916023 0 0)', + 'color(prophoto-rgb 0.7022480752 0.275720531 0.1035476647)', + 'color(rec2020 0.7919771358 0.2309756849 0.0737614751)', + ] + + for (let color of colors) { + let converted = convert(color) + let hue_distance = Math.min(converted.hue, 360 - converted.hue) + expect(hue_distance, `Failed hue for '${color}', got ${converted.hue}`).toBeLessThan(0.01) + expect( + converted.saturation, + `Failed saturation for '${color}', got ${converted.saturation}`, + ).toBeGreaterThan(99.9) + expect( + converted.lightness, + `Failed lightness for '${color}', got ${converted.lightness}`, + ).toBeCloseTo(50, 1) + expect(converted.alpha).toBe(1) + expect(converted.authored).toBe(color) + } + }) + const invalid_colors = ['hsl(0, 0, 0)', 'rgb(0 0 0 1)', 'rgb(a, b, c, 1)'] test.each(invalid_colors)(`invalid colors return a default object: %s`, (color) => { expect(convert(color)).toEqual({ diff --git a/index.ts b/index.ts index 3d042c6..6b56d10 100644 --- a/index.ts +++ b/index.ts @@ -1,56 +1,18 @@ -import { - tryColor, - ColorSpace, - XYZ_D65, - XYZ_D50, - XYZ_ABS_D65, - Lab_D65, - Lab, - LCH, - sRGB_Linear, - sRGB, - HSL, - HWB, - HSV, - P3_Linear, - P3, - A98RGB_Linear, - A98RGB, - ProPhoto_Linear, - ProPhoto, - REC_2020_Linear, - REC_2020, - OKLab, - OKLCH, - OKLrab, - to as convertColor, -} from 'colorjs.io/fn' - -// Register color spaces for parsing and converting -// TODO: According to the changelog we should be able to import -// and register all spaces in one go but it doesn't seem to work -ColorSpace.register(sRGB) // Parses keywords and hex colors -ColorSpace.register(XYZ_D65) -ColorSpace.register(XYZ_D50) -ColorSpace.register(XYZ_ABS_D65) -ColorSpace.register(Lab_D65) -ColorSpace.register(Lab) -ColorSpace.register(LCH) -ColorSpace.register(sRGB_Linear) -ColorSpace.register(HSL) -ColorSpace.register(HWB) -ColorSpace.register(HSV) -ColorSpace.register(P3_Linear) -ColorSpace.register(P3) -ColorSpace.register(A98RGB_Linear) -ColorSpace.register(A98RGB) -ColorSpace.register(ProPhoto_Linear) -ColorSpace.register(ProPhoto) -ColorSpace.register(REC_2020_Linear) -ColorSpace.register(REC_2020) -ColorSpace.register(OKLab) -ColorSpace.register(OKLCH) -ColorSpace.register(OKLrab) +import { colordx, extend } from '@colordx/core' +import lab from '@colordx/core/plugins/lab' +import lch from '@colordx/core/plugins/lch' +import hwb from '@colordx/core/plugins/hwb' +import hsv from '@colordx/core/plugins/hsv' +import p3 from '@colordx/core/plugins/p3' +import rec2020 from '@colordx/core/plugins/rec2020' +import a98rgb from '@colordx/core/plugins/a98rgb' +import prophoto from '@colordx/core/plugins/prophoto' +import names from '@colordx/core/plugins/names' + +// Register color spaces for parsing and converting. +// Not registered: `color(srgb ...)` / `color(srgb-linear ...)` function +// syntax, which @colordx/core doesn't support yet (colordx.dev roadmap). +extend([lab, lch, hwb, hsv, p3, rec2020, a98rgb, prophoto, names]) export type NormalizedColor = { hue: number @@ -75,9 +37,9 @@ function numerify(value: number | null | undefined): number { * @example convert('red') */ export function convert(authored: string): NormalizedColorWithAuthored { - let parsed = tryColor(authored) + let color = colordx(authored) - if (parsed === null) { + if (!color.isValid()) { return { hue: 0, saturation: 0, @@ -87,18 +49,13 @@ export function convert(authored: string): NormalizedColorWithAuthored { } } - let converted = parsed.space.id === 'hsl' ? parsed : convertColor(parsed, HSL) - let hsl = converted.coords - let hue = numerify(hsl[0]) - let saturation = numerify(hsl[1]) - let lightness = numerify(hsl[2]) - let alpha = numerify(converted.alpha) + let hsl = color.toHsl(15) return { - hue, - saturation, - lightness, - alpha, + hue: numerify(hsl.h), + saturation: numerify(hsl.s), + lightness: numerify(hsl.l), + alpha: numerify(hsl.alpha), authored, } } diff --git a/package.json b/package.json index 4928c24..d29ffc4 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "format": "oxfmt" }, "dependencies": { - "colorjs.io": "~0.7.1" + "@colordx/core": "^5.5.0" }, "devDependencies": { "@vitest/coverage-v8": "^4.1.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e846ee..a868e4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - colorjs.io: - specifier: ~0.7.1 - version: 0.7.1 + '@colordx/core': + specifier: ^5.5.0 + version: 5.5.0 devDependencies: '@vitest/coverage-v8': specifier: ^4.1.10 @@ -60,6 +60,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@colordx/core@5.5.0': + resolution: {integrity: sha512-3PxTH8itZzltK0U9jTwVVnjLXvnDYuq3m+QXsHkENxWiPRh4WaoLcs1SQjqgZ55kS+QyirpH5BVwzP2gMVG6EQ==} + '@emnapi/core@2.0.0-alpha.3': resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} @@ -757,9 +760,6 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - colorjs.io@0.7.1: - resolution: {integrity: sha512-LY7OHnJZxHwT5UlzNa9bbhHHDbzB6yE5+3MIPwJEQKRvSCt/T4G7epsj+9j2BExUIIfXFIJGsIXUKdfrK9Q5tA==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1236,6 +1236,8 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@colordx/core@5.5.0': {} + '@emnapi/core@2.0.0-alpha.3': dependencies: '@emnapi/wasi-threads': 2.0.1 @@ -1658,8 +1660,6 @@ snapshots: chai@6.2.2: {} - colorjs.io@0.7.1: {} - convert-source-map@2.0.0: {} defu@6.1.7: {} From 66eab87cd0db6dc998c83c8aa617dd7abcac4525 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 07:02:30 +0000 Subject: [PATCH 2/3] Extract HSL precision into a named constant --- index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 6b56d10..deb5c74 100644 --- a/index.ts +++ b/index.ts @@ -14,6 +14,11 @@ import names from '@colordx/core/plugins/names' // syntax, which @colordx/core doesn't support yet (colordx.dev roadmap). extend([lab, lch, hwb, hsv, p3, rec2020, a98rgb, prophoto, names]) +// High enough to preserve near-full float precision, since `compare()` +// relies on tiny lightness differences to break ties before falling +// back to alphabetical sorting. +const HSL_PRECISION = 15 + export type NormalizedColor = { hue: number saturation: number @@ -49,7 +54,7 @@ export function convert(authored: string): NormalizedColorWithAuthored { } } - let hsl = color.toHsl(15) + let hsl = color.toHsl(HSL_PRECISION) return { hue: numerify(hsl.h), From 0f0f0a916484f11bc1df8708c798ab4df293c4d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 07:13:42 +0000 Subject: [PATCH 3/3] Convert wide-gamut color parity test to test.each() --- index.test.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/index.test.ts b/index.test.ts index a0f658e..3e097b8 100644 --- a/index.test.ts +++ b/index.test.ts @@ -51,19 +51,19 @@ describe('convert', () => { } }) - test('the convert fn parses wide-gamut and CIE color spaces', () => { - // All of these are equivalent to `red` (#ff0000) - const colors = [ - 'lab(54.2905414047 80.8049283205 69.8909647686)', - 'lch(54.2905414047 106.8371817167 40.8576564524)', - 'hwb(0 0% 0%)', - 'color(display-p3 0.9174875573 0.2002868079 0.1385605911)', - 'color(a98-rgb 0.8585916023 0 0)', - 'color(prophoto-rgb 0.7022480752 0.275720531 0.1035476647)', - 'color(rec2020 0.7919771358 0.2309756849 0.0737614751)', - ] - - for (let color of colors) { + // All of these are equivalent to `red` (#ff0000) + const wide_gamut_colors = [ + 'lab(54.2905414047 80.8049283205 69.8909647686)', + 'lch(54.2905414047 106.8371817167 40.8576564524)', + 'hwb(0 0% 0%)', + 'color(display-p3 0.9174875573 0.2002868079 0.1385605911)', + 'color(a98-rgb 0.8585916023 0 0)', + 'color(prophoto-rgb 0.7022480752 0.275720531 0.1035476647)', + 'color(rec2020 0.7919771358 0.2309756849 0.0737614751)', + ] + test.each(wide_gamut_colors)( + 'the convert fn parses wide-gamut and CIE color spaces: %s', + (color) => { let converted = convert(color) let hue_distance = Math.min(converted.hue, 360 - converted.hue) expect(hue_distance, `Failed hue for '${color}', got ${converted.hue}`).toBeLessThan(0.01) @@ -77,8 +77,8 @@ describe('convert', () => { ).toBeCloseTo(50, 1) expect(converted.alpha).toBe(1) expect(converted.authored).toBe(color) - } - }) + }, + ) const invalid_colors = ['hsl(0, 0, 0)', 'rgb(0 0 0 1)', 'rgb(a, b, c, 1)'] test.each(invalid_colors)(`invalid colors return a default object: %s`, (color) => {