From 7c0e17078b317c1b5a1dc61854458db4da1e03da Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 3 Apr 2026 15:01:42 +0000 Subject: [PATCH 1/3] fix: color swatch not showing in style panel dropdown --- .../css-value-input/css-value-input.test.ts | 128 ++++++++++++++++++ .../css-value-input/css-value-input.tsx | 71 +++++++--- ...rse-intermediate-or-invalid-value.test.ts} | 0 3 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts rename apps/builder/app/builder/features/style-panel/shared/css-value-input/{parse-intermediate-or-invalid-value.ts.test.ts => parse-intermediate-or-invalid-value.test.ts} (100%) diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts new file mode 100644 index 000000000000..f54b1f452924 --- /dev/null +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts @@ -0,0 +1,128 @@ +import { describe, test, expect } from "vitest"; +import { __testing__ } from "./css-value-input"; +const { getItemColor, getItemUnit } = __testing__; + +describe("getItemColor", () => { + test("returns undefined for non-color keyword", () => { + expect(getItemColor({ type: "keyword", value: "auto" })).toBeUndefined(); + expect(getItemColor({ type: "keyword", value: "flex" })).toBeUndefined(); + }); + + test("returns the keyword value for a named color keyword", () => { + expect(getItemColor({ type: "keyword", value: "red" })).toBe("red"); + expect(getItemColor({ type: "keyword", value: "transparent" })).toBe( + "transparent" + ); + }); + + test("returns undefined for non-color var fallback types", () => { + expect( + getItemColor({ + type: "var", + value: "spacing", + fallback: { type: "unit", value: 8, unit: "px" }, + }) + ).toBeUndefined(); + expect( + getItemColor({ + type: "var", + value: "display", + fallback: { type: "keyword", value: "flex" }, + }) + ).toBeUndefined(); + }); + + test("returns color string for rgb var fallback", () => { + expect( + getItemColor({ + type: "var", + value: "brand", + fallback: { type: "rgb", r: 255, g: 0, b: 0, alpha: 1 }, + }) + ).toBe("rgb(255 0 0 / 1)"); + }); + + test("returns color string for color var fallback", () => { + const result = getItemColor({ + type: "var", + value: "brand", + fallback: { + type: "color", + colorSpace: "srgb", + components: [1, 0, 0], + alpha: 1, + }, + }); + expect(result).toMatch(/rgb/); + }); + + test("returns value for unparsed var fallback that is a valid color", () => { + expect( + getItemColor({ + type: "var", + value: "brand", + fallback: { type: "unparsed", value: "red" }, + }) + ).toBe("red"); + }); + + test("returns undefined for unparsed var fallback that is not a color", () => { + expect( + getItemColor({ + type: "var", + value: "spacing", + fallback: { type: "unparsed", value: "1rem" }, + }) + ).toBeUndefined(); + }); + + test("returns value for keyword var fallback that is a named color", () => { + expect( + getItemColor({ + type: "var", + value: "brand", + fallback: { type: "keyword", value: "red" }, + }) + ).toBe("red"); + }); + + test("returns undefined for var without fallback", () => { + expect(getItemColor({ type: "var", value: "brand" })).toBeUndefined(); + }); + + test("returns undefined for unit item", () => { + expect( + getItemColor({ type: "unit", value: 16, unit: "px" }) + ).toBeUndefined(); + }); +}); + +describe("getItemUnit", () => { + test("returns unit string for var with unit fallback", () => { + expect( + getItemUnit({ + type: "var", + value: "spacing", + fallback: { type: "unit", value: 8, unit: "px" }, + }) + ).toBe("8px"); + }); + + test("returns undefined for var with non-unit fallback", () => { + expect( + getItemUnit({ + type: "var", + value: "brand", + fallback: { type: "keyword", value: "red" }, + }) + ).toBeUndefined(); + }); + + test("returns undefined for var without fallback", () => { + expect(getItemUnit({ type: "var", value: "spacing" })).toBeUndefined(); + }); + + test("returns undefined for keyword item", () => { + expect(getItemUnit({ type: "keyword", value: "auto" })).toBeUndefined(); + }); +}); diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index d4175be0b176..75b079435ad0 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -45,6 +45,7 @@ import { camelCaseProperty, declarationDescriptions, isValidDeclaration, + parseColor, } from "@webstudio-is/css-data"; import { $selectedInstanceSizes } from "~/shared/nano-states"; import { convertUnits } from "./convert-units"; @@ -363,6 +364,37 @@ const itemToString = (item: CssValueInputValue | null) => { const Description = styled(Box, { width: theme.spacing[27] }); +// Returns the CSS color string to show as a color swatch for a dropdown item, +// or undefined if the item has no meaningful color preview. +const getItemColor = (item: CssValueInputValue): string | undefined => { + if (item.type === "var") { + const { fallback } = item; + if (fallback?.type === "rgb" || fallback?.type === "color") { + return toValue(fallback); + } + if ( + (fallback?.type === "unparsed" || fallback?.type === "keyword") && + parseColor(fallback.value) !== undefined + ) { + return fallback.value; + } + return undefined; + } + if (item.type === "keyword" && parseColor(item.value) !== undefined) { + return item.value; + } + return undefined; +}; + +// Returns the unit string to show as a text preview for a var dropdown item, +// or undefined if the fallback is not a unit. +const getItemUnit = (item: CssValueInputValue): string | undefined => { + if (item.type === "var" && item.fallback?.type === "unit") { + return toValue(item.fallback); + } + return undefined; +}; + /** * Common: * - Free text editing @@ -943,22 +975,27 @@ export const CssValueInput = ({ {...getItemProps({ item, index })} key={index} > - {item.type === "var" ? ( - - --{item.value} - {item.fallback?.type === "unit" && ( - - {toValue(item.fallback)} - - )} - {(item.fallback?.type === "rgb" || - item.fallback?.type === "color") && ( - - )} - - ) : ( - itemToString(item) - )} + {(() => { + const label = itemToString(item); + const colorValue = getItemColor(item); + const unitValue = getItemUnit(item); + if (colorValue === undefined && unitValue === undefined) { + return label; + } + return ( + + {label} + {unitValue !== undefined && ( + + {unitValue} + + )} + {colorValue !== undefined && ( + + )} + + ); + })()} ))} @@ -974,3 +1011,5 @@ export const CssValueInput = ({ ); }; + +export const __testing__ = { getItemColor, getItemUnit }; diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.ts.test.ts b/apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.test.ts similarity index 100% rename from apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.ts.test.ts rename to apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.test.ts From b2b131d3c95217395c43a8d22ba7aa5f38160114 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 3 Apr 2026 15:11:28 +0000 Subject: [PATCH 2/3] refactor: simplify getItemColor using toValue like ColorPicker does --- .../css-value-input/css-value-input.tsx | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index 75b079435ad0..99a72bc08a32 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -367,23 +367,15 @@ const Description = styled(Box, { width: theme.spacing[27] }); // Returns the CSS color string to show as a color swatch for a dropdown item, // or undefined if the item has no meaningful color preview. const getItemColor = (item: CssValueInputValue): string | undefined => { - if (item.type === "var") { - const { fallback } = item; - if (fallback?.type === "rgb" || fallback?.type === "color") { - return toValue(fallback); - } - if ( - (fallback?.type === "unparsed" || fallback?.type === "keyword") && - parseColor(fallback.value) !== undefined - ) { - return fallback.value; - } - return undefined; + let colorString: string | undefined; + if (item.type === "var" && item.fallback !== undefined) { + colorString = toValue(item.fallback); + } else if (item.type === "keyword") { + colorString = item.value; } - if (item.type === "keyword" && parseColor(item.value) !== undefined) { - return item.value; + if (colorString !== undefined && parseColor(colorString) !== undefined) { + return colorString; } - return undefined; }; // Returns the unit string to show as a text preview for a var dropdown item, @@ -392,7 +384,6 @@ const getItemUnit = (item: CssValueInputValue): string | undefined => { if (item.type === "var" && item.fallback?.type === "unit") { return toValue(item.fallback); } - return undefined; }; /** From 0cb32dc273636d5d3fcd562c7c5739ee779c5c5d Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 3 Apr 2026 15:16:20 +0000 Subject: [PATCH 3/3] revert: remove unasked unit fallback preview from dropdown items --- .../css-value-input/css-value-input.test.ts | 32 +------------------ .../css-value-input/css-value-input.tsx | 23 ++----------- 2 files changed, 4 insertions(+), 51 deletions(-) diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts index f54b1f452924..761a206496cd 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from "vitest"; import { __testing__ } from "./css-value-input"; -const { getItemColor, getItemUnit } = __testing__; +const { getItemColor } = __testing__; describe("getItemColor", () => { test("returns undefined for non-color keyword", () => { @@ -96,33 +96,3 @@ describe("getItemColor", () => { ).toBeUndefined(); }); }); - -describe("getItemUnit", () => { - test("returns unit string for var with unit fallback", () => { - expect( - getItemUnit({ - type: "var", - value: "spacing", - fallback: { type: "unit", value: 8, unit: "px" }, - }) - ).toBe("8px"); - }); - - test("returns undefined for var with non-unit fallback", () => { - expect( - getItemUnit({ - type: "var", - value: "brand", - fallback: { type: "keyword", value: "red" }, - }) - ).toBeUndefined(); - }); - - test("returns undefined for var without fallback", () => { - expect(getItemUnit({ type: "var", value: "spacing" })).toBeUndefined(); - }); - - test("returns undefined for keyword item", () => { - expect(getItemUnit({ type: "keyword", value: "auto" })).toBeUndefined(); - }); -}); diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index 99a72bc08a32..3d614f8e2d82 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -16,7 +16,6 @@ import { theme, Flex, styled, - Text, ColorThumb, } from "@webstudio-is/design-system"; import type { @@ -378,14 +377,6 @@ const getItemColor = (item: CssValueInputValue): string | undefined => { } }; -// Returns the unit string to show as a text preview for a var dropdown item, -// or undefined if the fallback is not a unit. -const getItemUnit = (item: CssValueInputValue): string | undefined => { - if (item.type === "var" && item.fallback?.type === "unit") { - return toValue(item.fallback); - } -}; - /** * Common: * - Free text editing @@ -969,21 +960,13 @@ export const CssValueInput = ({ {(() => { const label = itemToString(item); const colorValue = getItemColor(item); - const unitValue = getItemUnit(item); - if (colorValue === undefined && unitValue === undefined) { + if (colorValue === undefined) { return label; } return ( {label} - {unitValue !== undefined && ( - - {unitValue} - - )} - {colorValue !== undefined && ( - - )} + ); })()} @@ -1003,4 +986,4 @@ export const CssValueInput = ({ ); }; -export const __testing__ = { getItemColor, getItemUnit }; +export const __testing__ = { getItemColor };