Skip to content

Commit 942b81b

Browse files
authored
refactor: upgrade colorjs.io to v0.6 and adopt functional API (#5617)
## Summary Upgrades colorjs.io from v0.5.x to v0.6.x and adopts the new functional API to fix TypeScript compatibility issues and reduce bundle size. ## Breaking Changes in v0.6 This release includes two major changes from colorjs.io that required code updates: 1. `null` instead of `NaN` for "none" values — Coordinates now use `number | null` instead of the custom `Number` instance that previously used `"none"` strings. This eliminates runtime casting and improves TypeScript DX. 2. Plain numbers instead of `Number` objects — Coordinates are now plain number primitives rather than `Number` objects with parsing metadata, improving performance and bundle size. ## Changes Made - Upgraded colorjs.io to v0.6.x - Adopted functional API (`/fn` entry point) for tree-shakeable imports and reduced bundle size - Updated coordinate handling to use `number | null` directly instead of custom `Number` instances with `"none"` values - Removed runtime casting previously required for TypeScript compatibility - Aligned with engramma and hdr-color-input — both already use the functional style, enabling code deduplication ## Migration Notes See [colorjs.io v0.6.0 release notes](https://github.com/color-js/color.js/releases/tag/v0.6.0) for full details on the breaking changes.
1 parent c60f8d7 commit 942b81b

File tree

9 files changed

+88
-48
lines changed

9 files changed

+88
-48
lines changed

apps/builder/app/builder/shared/css-editor/css-editor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mergeRefs } from "@react-aria/utils";
2-
import Color from "colorjs.io";
2+
import * as colorjs from "colorjs.io/fn";
33
import {
44
memo,
55
useEffect,
@@ -134,7 +134,7 @@ const AdvancedPropertyValue = ({
134134
const inputRef = useRef<HTMLInputElement>(null);
135135
let isColor = false;
136136
try {
137-
new Color(toValue(styleDecl.usedValue));
137+
colorjs.parse(toValue(styleDecl.usedValue));
138138
isColor = true;
139139
} catch {
140140
isColor = false;

apps/builder/app/canvas/features/text-editor/text-editor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as colorjs from "colorjs.io/fn";
12
import {
23
useState,
34
useEffect,
@@ -60,7 +61,6 @@ import {
6061
import { isDescendantOrSelf, type InstanceSelector } from "~/shared/tree-utils";
6162
import { ToolbarConnectorPlugin } from "./toolbar-connector";
6263
import { type Refs, $convertToLexical, $convertToUpdates } from "./interop";
63-
import Color from "colorjs.io";
6464
import { useEffectEvent } from "~/shared/hook-utils/effect-event";
6565
import {
6666
deleteInstanceMutable,
@@ -146,7 +146,7 @@ const CaretColorPlugin = () => {
146146

147147
let isLightBackground = false;
148148
try {
149-
const color = new Color(elementColor);
149+
const color = colorjs.parse(elementColor);
150150
const alpha = color.alpha ?? 1;
151151
isLightBackground = alpha < 0.1;
152152
} catch {

apps/builder/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"args-tokenizer": "^0.3.0",
8787
"bcp-47": "^2.1.0",
8888
"change-case": "^5.4.4",
89-
"colorjs.io": "^0.5.0",
89+
"colorjs.io": "^0.6.1",
9090
"cookie": "^1.0.1",
9191
"css-tree": "^3.1.0",
9292
"debug": "^4.3.7",

packages/css-data/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"dependencies": {
3535
"@webstudio-is/css-engine": "workspace:*",
3636
"change-case": "^5.4.4",
37-
"colorjs.io": "^0.5.0",
37+
"colorjs.io": "^0.6.1",
3838
"css-tree": "^3.1.0",
3939
"openai": "^3.2.1",
4040
"p-retry": "^6.2.1",

packages/css-data/src/parse-css-value.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Color from "colorjs.io";
1+
import * as colorjs from "colorjs.io/fn";
22
import {
33
type CssNode,
44
type FunctionNode,
@@ -30,6 +30,21 @@ import {
3030
import { keywordValues } from "./__generated__/keyword-values";
3131
import { units } from "./__generated__/units";
3232

33+
colorjs.ColorSpace.register(colorjs.sRGB);
34+
colorjs.ColorSpace.register(colorjs.sRGB_Linear);
35+
colorjs.ColorSpace.register(colorjs.HSL);
36+
colorjs.ColorSpace.register(colorjs.HWB);
37+
colorjs.ColorSpace.register(colorjs.Lab);
38+
colorjs.ColorSpace.register(colorjs.LCH);
39+
colorjs.ColorSpace.register(colorjs.OKLab);
40+
colorjs.ColorSpace.register(colorjs.OKLCH);
41+
colorjs.ColorSpace.register(colorjs.P3);
42+
colorjs.ColorSpace.register(colorjs.A98RGB);
43+
colorjs.ColorSpace.register(colorjs.ProPhoto);
44+
colorjs.ColorSpace.register(colorjs.REC_2020);
45+
colorjs.ColorSpace.register(colorjs.XYZ_D65);
46+
colorjs.ColorSpace.register(colorjs.XYZ_D50);
47+
3348
export const cssTryParseValue = (input: string): undefined | CssNode => {
3449
try {
3550
const ast = parse(input, { context: "value" });
@@ -165,16 +180,16 @@ const colorSpace: Record<string, ColorValue["colorSpace"]> = {
165180
xyz: "xyz-d65", // default to d65
166181
};
167182

168-
const toColorComponent = (value: number) =>
169-
Math.round(value.valueOf() * 10000) / 10000;
183+
const toColorComponent = (value: undefined | null | number) =>
184+
Math.round((value ?? 0) * 10000) / 10000;
170185

171186
export const parseColor = (colorString: string): undefined | ColorValue => {
172187
// does not match css variables which are incorrectly treated by colorjs.io
173188
if (!lexer.match("<color>", colorString).matched) {
174189
return;
175190
}
176191
try {
177-
const color = new Color(colorString);
192+
const color = colorjs.parse(colorString);
178193
return {
179194
type: "color",
180195
colorSpace: colorSpace[color.spaceId],

packages/design-system/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"@webstudio-is/icons": "workspace:*",
6464
"change-case": "^5.4.4",
6565
"cmdk": "^1.1.1",
66-
"colorjs.io": "^0.5.2",
66+
"colorjs.io": "^0.6.1",
6767
"downshift": "^6.1.7",
6868
"match-sorter": "^8.0.0",
6969
"react-colorful": "^5.6.1",

packages/design-system/src/components/color-picker.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import * as colorjs from "colorjs.io/fn";
12
import {
23
forwardRef,
34
type ComponentProps,
45
type ElementRef,
56
useEffect,
67
useState,
78
} from "react";
8-
import Color from "colorjs.io";
99
import { clamp } from "@react-aria/utils";
1010
import { useDebouncedCallback } from "use-debounce";
1111
import { RgbaColorPicker } from "react-colorful";
@@ -23,6 +23,21 @@ import { IconButton } from "./icon-button";
2323
import { InputField } from "./input-field";
2424
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
2525

26+
colorjs.ColorSpace.register(colorjs.sRGB);
27+
colorjs.ColorSpace.register(colorjs.sRGB_Linear);
28+
colorjs.ColorSpace.register(colorjs.HSL);
29+
colorjs.ColorSpace.register(colorjs.HWB);
30+
colorjs.ColorSpace.register(colorjs.Lab);
31+
colorjs.ColorSpace.register(colorjs.LCH);
32+
colorjs.ColorSpace.register(colorjs.OKLab);
33+
colorjs.ColorSpace.register(colorjs.OKLCH);
34+
colorjs.ColorSpace.register(colorjs.P3);
35+
colorjs.ColorSpace.register(colorjs.A98RGB);
36+
colorjs.ColorSpace.register(colorjs.ProPhoto);
37+
colorjs.ColorSpace.register(colorjs.REC_2020);
38+
colorjs.ColorSpace.register(colorjs.XYZ_D65);
39+
colorjs.ColorSpace.register(colorjs.XYZ_D50);
40+
2641
type RgbaColor = {
2742
r: number;
2843
g: number;
@@ -31,12 +46,12 @@ type RgbaColor = {
3146
};
3247

3348
// Helper to create RgbaColor from colorjs.io Color
34-
const colorToRgba = (color: Color): RgbaColor => {
49+
const colorToRgba = (color: colorjs.PlainColorObject): RgbaColor => {
3550
const [r, g, b] = color.coords;
3651
return {
37-
r: r * 255,
38-
g: g * 255,
39-
b: b * 255,
52+
r: (r ?? 0) * 255,
53+
g: (g ?? 0) * 255,
54+
b: (b ?? 0) * 255,
4055
a: color.alpha ?? 1,
4156
};
4257
};
@@ -46,8 +61,7 @@ const transparentColor: RgbaColor = { r: 0, g: 0, b: 0, a: 0 };
4661
// Helper to parse color string to RgbaColor
4762
export const parseColorString = (colorString: string): RgbaColor => {
4863
try {
49-
const color = new Color(colorString);
50-
return colorToRgba(color.to("srgb"));
64+
return colorToRgba(colorjs.to(colorString, "srgb"));
5165
} catch {
5266
return transparentColor;
5367
}
@@ -81,7 +95,7 @@ const colorfulStyles = css({
8195

8296
const whiteColor: RgbaColor = { r: 255, g: 255, b: 255, a: 1 };
8397
const borderColorSwatch = colorToRgba(
84-
new Color(rawTheme.colors.borderColorSwatch)
98+
colorjs.to(rawTheme.colors.borderColorSwatch, "srgb")
8599
);
86100

87101
const distance = (a: RgbaColor, b: RgbaColor) =>
@@ -281,7 +295,10 @@ export const ColorPicker = ({
281295
onChange={(event) => {
282296
setHex(event.target.value);
283297
try {
284-
const color = new Color(normalizeHex(event.target.value));
298+
const color = colorjs.to(
299+
normalizeHex(event.target.value),
300+
"srgb"
301+
);
285302
const rgba = colorToRgba(color);
286303
const newValue = colorResultToRgbValue(rgba);
287304
onChange(newValue);

packages/design-system/src/components/gradient-picker.tsx

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {
1919
GradientStop,
2020
ParsedGradient,
2121
} from "@webstudio-is/css-data";
22-
import Color from "colorjs.io";
22+
import * as colorjs from "colorjs.io/fn";
2323
import { ChevronFilledUpIcon } from "@webstudio-is/icons";
2424
import { styled, theme } from "../stitches.config";
2525
import { Flex } from "./flex";
@@ -32,23 +32,31 @@ const mixColors = (
3232
color2: RgbValue,
3333
ratio: number
3434
): RgbValue => {
35-
const c1 = new Color("srgb", [
36-
color1.r / 255,
37-
color1.g / 255,
38-
color1.b / 255,
39-
]);
40-
const c2 = new Color("srgb", [
41-
color2.r / 255,
42-
color2.g / 255,
43-
color2.b / 255,
44-
]);
45-
const mixed = c1.mix(c2, ratio);
35+
const c1: colorjs.ColorConstructor = {
36+
spaceId: "srgb",
37+
coords: [
38+
(color1.r ?? 0) / 255,
39+
(color1.g ?? 0) / 255,
40+
(color1.b ?? 0) / 255,
41+
],
42+
alpha: undefined,
43+
};
44+
const c2: colorjs.ColorConstructor = {
45+
spaceId: "srgb",
46+
coords: [
47+
(color2.r ?? 0) / 255,
48+
(color2.g ?? 0) / 255,
49+
(color2.b ?? 0) / 255,
50+
],
51+
alpha: undefined,
52+
};
53+
const mixed = colorjs.mix(c1, c2, ratio);
4654
const [r, g, b] = mixed.coords;
4755
return {
4856
type: "rgb",
49-
r: r * 255,
50-
g: g * 255,
51-
b: b * 255,
57+
r: (r ?? 0) * 255,
58+
g: (g ?? 0) * 255,
59+
b: (b ?? 0) * 255,
5260
alpha: color1.alpha ?? 1,
5361
};
5462
};
@@ -88,14 +96,14 @@ const toRgbColor = (
8896
}
8997

9098
try {
91-
const parsed = new Color(toValue(color));
99+
const parsed = colorjs.parse(toValue(color));
92100
const [r, g, b] = parsed.coords;
93101
const alpha = parsed.alpha;
94102
return {
95103
type: "rgb",
96-
r: r * 255,
97-
g: g * 255,
98-
b: b * 255,
104+
r: (r ?? 0) * 255,
105+
g: (g ?? 0) * 255,
106+
b: (b ?? 0) * 255,
99107
alpha: alpha ?? 1,
100108
};
101109
} catch {

pnpm-lock.yaml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)