Skip to content

Commit 21bce61

Browse files
TrySoundkof
andauthored
feat: support modern color spaces in color inputs (#5465)
Ref #3574 Here improved css value parser to parse and store any color space. oklch(), hsl(), color() and others can be used in any color input. Color picker converts to rgb on the fly and do not support any other color space at the moment. --------- Co-authored-by: Oleg Isonen <[email protected]>
1 parent 2428bb7 commit 21bce61

File tree

23 files changed

+931
-281
lines changed

23 files changed

+931
-281
lines changed

apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-utils.test.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ describe("convertGradientToTarget", () => {
228228
conic: "conic-gradient(red 0%, blue 100%)",
229229
radial: "radial-gradient(circle, red 0%, blue 100%)",
230230
};
231-
const expectedColors = ["rgb(255 0 0 / 1)", "rgb(0 0 255 / 1)"];
232231
const gradientTypes: GradientType[] = ["linear", "conic", "radial"];
233232

234233
const getStopColors = (stops: GradientStop[]) =>
@@ -239,6 +238,8 @@ describe("convertGradientToTarget", () => {
239238
)
240239
.map((color) => toValue(color));
241240

241+
const expectedColorValues = ["rgb(255 0 0 / 1)", "rgb(0 0 255 / 1)"];
242+
242243
gradientTypes.forEach((sourceType) => {
243244
gradientTypes.forEach((targetType) => {
244245
test(`converts ${sourceType} gradient to ${targetType}`, () => {
@@ -248,8 +249,8 @@ describe("convertGradientToTarget", () => {
248249
};
249250
const converted = convertGradientToTarget(styleValue, targetType);
250251
expect(converted.type).toBe(targetType);
251-
expect(converted.stops).toHaveLength(expectedColors.length);
252-
expect(getStopColors(converted.stops)).toEqual(expectedColors);
252+
expect(converted.stops).toHaveLength(expectedColorValues.length);
253+
expect(getStopColors(converted.stops)).toEqual(expectedColorValues);
253254
});
254255
});
255256
});
@@ -541,17 +542,15 @@ describe("ensureGradientHasStops", () => {
541542
const result = ensureGradientHasStops(gradient);
542543
expect(result.stops).toHaveLength(2);
543544
expect(result.stops[0]?.color).toEqual({
544-
type: "rgb",
545-
r: 0,
546-
g: 0,
547-
b: 0,
545+
type: "color",
546+
colorSpace: "srgb",
547+
components: [0, 0, 0],
548548
alpha: 1,
549549
});
550550
expect(result.stops[1]?.color).toEqual({
551-
type: "rgb",
552-
r: 0,
553-
g: 0,
554-
b: 0,
551+
type: "color",
552+
colorSpace: "srgb",
553+
components: [0, 0, 0],
555554
alpha: 0,
556555
});
557556
});
@@ -568,10 +567,9 @@ describe("ensureGradientHasStops", () => {
568567

569568
const result = ensureGradientHasStops(gradient);
570569
expect(result.stops[0]?.color).toEqual({
571-
type: "rgb",
572-
r: 0,
573-
g: 0,
574-
b: 0,
570+
type: "color",
571+
colorSpace: "srgb",
572+
components: [0, 0, 0],
575573
alpha: 1,
576574
});
577575
});
@@ -639,10 +637,9 @@ describe("styleValueToColor", () => {
639637
value: "#0000ff",
640638
};
641639
expect(styleValueToColor(style)).toEqual({
642-
type: "rgb",
643-
r: 0,
644-
g: 0,
645-
b: 255,
640+
type: "color",
641+
colorSpace: "srgb",
642+
components: [0, 0, 1],
646643
alpha: 1,
647644
});
648645
});
@@ -658,10 +655,9 @@ describe("styleValueToColor", () => {
658655
test("parses invalid style when value is valid color string", () => {
659656
const style: StyleValue = { type: "invalid", value: "rgb(10 20 30)" };
660657
expect(styleValueToColor(style)).toEqual({
661-
type: "rgb",
662-
r: 10,
663-
g: 20,
664-
b: 30,
658+
type: "color",
659+
colorSpace: "srgb",
660+
components: [0.0392, 0.0784, 0.1176],
665661
alpha: 1,
666662
});
667663
});
@@ -677,10 +673,9 @@ describe("styleValueToColor", () => {
677673
value: "hsl(180 100% 50%)",
678674
};
679675
expect(styleValueToColor(style)).toEqual({
680-
type: "rgb",
681-
r: 0,
682-
g: 255,
683-
b: 255,
676+
type: "color",
677+
colorSpace: "hsl",
678+
components: [180, 100, 50],
684679
alpha: 1,
685680
});
686681
});

apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-utils.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { clamp } from "@react-aria/utils";
22
import {
3+
ColorValue,
34
toValue,
45
type CssProperty,
56
type KeywordValue,
6-
type RgbValue,
77
type StyleValue,
88
type Unit,
99
type UnitValue,
@@ -233,21 +233,27 @@ const toPercentUnitValue = (value: UnitValue): PercentUnitValue | undefined => {
233233
} satisfies PercentUnitValue;
234234
};
235235

236-
export const fallbackStopColor: RgbValue = {
237-
type: "rgb",
238-
r: 0,
239-
g: 0,
240-
b: 0,
236+
export const fallbackStopColor: ColorValue = {
237+
type: "color",
238+
colorSpace: "srgb",
239+
components: [0, 0, 0],
241240
alpha: 1,
242241
};
243242

243+
const transparentColor: ColorValue = {
244+
type: "color",
245+
colorSpace: "srgb",
246+
components: [0, 0, 0],
247+
alpha: 0,
248+
};
249+
244250
export const createDefaultStops = (): GradientStop[] => [
245251
{
246-
color: { type: "rgb", r: 0, g: 0, b: 0, alpha: 1 },
252+
color: fallbackStopColor,
247253
position: { type: "unit", unit: "%", value: 0 },
248254
},
249255
{
250-
color: { type: "rgb", r: 0, g: 0, b: 0, alpha: 0 },
256+
color: transparentColor,
251257
position: { type: "unit", unit: "%", value: 100 },
252258
},
253259
];
@@ -1125,6 +1131,7 @@ export const updateGradientStop = <T extends ParsedGradient>(
11251131
const parseColorString = (value: string): GradientStop["color"] | undefined => {
11261132
const parsed = parseCssValue("color", value);
11271133
if (
1134+
parsed.type === "color" ||
11281135
parsed.type === "rgb" ||
11291136
parsed.type === "keyword" ||
11301137
parsed.type === "var"
@@ -1144,7 +1151,7 @@ export const styleValueToColor = (
11441151
return parseColorString(styleValue.value);
11451152
}
11461153

1147-
if (styleValue.type === "rgb") {
1154+
if (styleValue.type === "rgb" || styleValue.type === "color") {
11481155
return styleValue;
11491156
}
11501157

apps/builder/app/builder/features/style-panel/shared/color-picker.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export const ColorPickerControl = ({
7777
}
7878
if (
7979
styleValue.type === "rgb" ||
80+
styleValue.type === "color" ||
8081
styleValue.type === "keyword" ||
8182
styleValue.type === "var" ||
8283
styleValue.type === "invalid"
@@ -101,6 +102,7 @@ export const ColorPickerControl = ({
101102
onChangeComplete={({ value }) => {
102103
if (
103104
value.type === "rgb" ||
105+
value.type === "color" ||
104106
value.type === "keyword" ||
105107
value.type === "var"
106108
) {

apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,8 @@ export const CssValueInput = ({
932932
{toValue(item.fallback)}
933933
</Text>
934934
)}
935-
{item.fallback?.type === "rgb" && (
935+
{(item.fallback?.type === "rgb" ||
936+
item.fallback?.type === "color") && (
936937
<ColorThumb color={toValue(item.fallback)} />
937938
)}
938939
</Flex>

apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.ts.test.ts

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -454,11 +454,10 @@ describe("Colors", () => {
454454
});
455455

456456
expect(result).toEqual({
457-
type: "rgb",
457+
type: "color",
458458
alpha: 0.5,
459-
r: 10,
460-
g: 20,
461-
b: 30,
459+
colorSpace: "srgb",
460+
components: [0.0392, 0.0784, 0.1176],
462461
});
463462
});
464463

@@ -469,11 +468,10 @@ describe("Colors", () => {
469468
});
470469

471470
expect(result).toEqual({
472-
type: "rgb",
471+
type: "color",
473472
alpha: 1,
474-
r: 255,
475-
g: 0,
476-
b: 0,
473+
colorSpace: "srgb",
474+
components: [1, 0, 0],
477475
});
478476
});
479477

@@ -484,11 +482,10 @@ describe("Colors", () => {
484482
});
485483

486484
expect(result).toEqual({
487-
type: "rgb",
485+
type: "color",
488486
alpha: 1,
489-
r: 240,
490-
g: 238,
491-
b: 15,
487+
colorSpace: "srgb",
488+
components: [0.9412, 0.9333, 0.0588],
492489
});
493490
});
494491

@@ -499,11 +496,10 @@ describe("Colors", () => {
499496
});
500497

501498
expect(result).toEqual({
502-
type: "rgb",
499+
type: "color",
503500
alpha: 1,
504-
r: 255,
505-
g: 0,
506-
b: 0,
501+
colorSpace: "srgb",
502+
components: [1, 0, 0],
507503
});
508504
});
509505

@@ -514,11 +510,10 @@ describe("Colors", () => {
514510
});
515511

516512
expect(result).toEqual({
517-
type: "rgb",
513+
type: "color",
518514
alpha: 1,
519-
r: 240,
520-
g: 238,
521-
b: 15,
515+
colorSpace: "srgb",
516+
components: [0.9412, 0.9333, 0.0588],
522517
});
523518
});
524519

@@ -530,11 +525,10 @@ describe("Colors", () => {
530525
});
531526

532527
expect(result).toEqual({
533-
type: "rgb",
528+
type: "color",
534529
alpha: 0.5,
535-
r: 10,
536-
g: 20,
537-
b: 30,
530+
colorSpace: "srgb",
531+
components: [0.0392, 0.0784, 0.1176],
538532
});
539533
});
540534
});
@@ -600,10 +594,9 @@ test("parse color in css variable", () => {
600594
value: "#0f0f0f",
601595
})
602596
).toEqual({
603-
type: "rgb",
604-
r: 15,
605-
g: 15,
606-
b: 15,
597+
type: "color",
598+
colorSpace: "srgb",
599+
components: [0.0588, 0.0588, 0.0588],
607600
alpha: 1,
608601
});
609602
});

apps/builder/app/builder/features/style-panel/shared/model.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,24 @@ export const $availableVariables = computed(
320320
export const $availableUnitVariables = computed(
321321
$availableVariables,
322322
(availableVariables) =>
323-
availableVariables.filter((value) => value.fallback?.type !== "rgb")
323+
availableVariables.filter(
324+
(value) =>
325+
value.fallback?.type === "unit" ||
326+
value.fallback?.type === "keyword" ||
327+
value.fallback?.type === "unparsed"
328+
)
324329
);
325330

326331
export const $availableColorVariables = computed(
327332
$availableVariables,
328333
(availableVariables) =>
329-
availableVariables.filter((value) => value.fallback?.type !== "unit")
334+
availableVariables.filter(
335+
(value) =>
336+
value.fallback?.type === "rgb" ||
337+
value.fallback?.type === "color" ||
338+
value.fallback?.type === "keyword" ||
339+
value.fallback?.type === "unparsed"
340+
)
330341
);
331342

332343
export const createComputedStyleDeclStore = (property: CssProperty) => {

apps/builder/app/builder/features/style-panel/shared/shadow-content.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,20 @@ export const ShadowContent = ({
341341
...$availableColorVariables.get(),
342342
]}
343343
onChange={(value) => {
344-
if (value.type === "rgb" || value.type === "var") {
344+
if (
345+
value.type === "rgb" ||
346+
value.type === "color" ||
347+
value.type === "var"
348+
) {
345349
updateShadow({ color: value }, { isEphemeral: true });
346350
}
347351
}}
348352
onChangeComplete={(value) => {
349-
if (value.type === "rgb" || value.type === "var") {
353+
if (
354+
value.type === "rgb" ||
355+
value.type === "color" ||
356+
value.type === "var"
357+
) {
350358
updateShadow({ color: value });
351359
}
352360
}}

0 commit comments

Comments
 (0)